diff --git a/.eslintrc.js b/.eslintrc.js index 4959b133a0..9ae51f9bc5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,24 @@ module.exports = { "quotes": "off", "no-extra-boolean-cast": "off", + "no-restricted-properties": [ + "error", + ...buildRestrictedPropertiesOptions( + ["window.innerHeight", "window.innerWidth", "window.visualViewport"], + "Use UIStore to access window dimensions instead", + ), + ], }, }], }; + +function buildRestrictedPropertiesOptions(properties, message) { + return properties.map(prop => { + const [object, property] = prop.split("."); + return { + object, + property, + message, + }; + }); +} diff --git a/CHANGELOG.md b/CHANGELOG.md index d459b4e94a..f3d9afd51d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,338 @@ +Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0) + + * Upgrade to JS SDK 11.1.0 + * [Release] Bump libolm version + [\#6087](https://github.com/matrix-org/matrix-react-sdk/pull/6087) + +Changes in [3.22.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0-rc.1) (2021-05-19) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0...v3.22.0-rc.1) + + * Upgrade to JS SDK 11.1.0-rc.1 + * Translations update from Weblate + [\#6068](https://github.com/matrix-org/matrix-react-sdk/pull/6068) + * Show DMs in space for invited members too, to match Android impl + [\#6062](https://github.com/matrix-org/matrix-react-sdk/pull/6062) + * Support filtering by alias in add existing to space dialog + [\#6057](https://github.com/matrix-org/matrix-react-sdk/pull/6057) + * Fix issue when a room without a name or alias is marked as suggested + [\#6064](https://github.com/matrix-org/matrix-react-sdk/pull/6064) + * Fix space room hierarchy not updating when removing a room + [\#6055](https://github.com/matrix-org/matrix-react-sdk/pull/6055) + * Revert "Try putting room list handling behind a lock" + [\#6060](https://github.com/matrix-org/matrix-react-sdk/pull/6060) + * Stop assuming encrypted messages are decrypted ahead of time + [\#6052](https://github.com/matrix-org/matrix-react-sdk/pull/6052) + * Add error detail when languges fail to load + [\#6059](https://github.com/matrix-org/matrix-react-sdk/pull/6059) + * Add space invaders chat effect + [\#6053](https://github.com/matrix-org/matrix-react-sdk/pull/6053) + * Create SpaceProvider and hide Spaces from the RoomProvider autocompleter + [\#6051](https://github.com/matrix-org/matrix-react-sdk/pull/6051) + * Don't mark a room as unread when redacted event is present + [\#6049](https://github.com/matrix-org/matrix-react-sdk/pull/6049) + * Add support for MSC2873: Client information for Widgets + [\#6023](https://github.com/matrix-org/matrix-react-sdk/pull/6023) + * Support UI for MSC2762: Widgets reading events from rooms + [\#5960](https://github.com/matrix-org/matrix-react-sdk/pull/5960) + * Fix crash on opening notification panel + [\#6047](https://github.com/matrix-org/matrix-react-sdk/pull/6047) + * Remove custom LoggedInView::shouldComponentUpdate logic + [\#6046](https://github.com/matrix-org/matrix-react-sdk/pull/6046) + * Fix edge cases with the new add reactions prompt button + [\#6045](https://github.com/matrix-org/matrix-react-sdk/pull/6045) + * Add ids to homeserver and passphrase fields + [\#6043](https://github.com/matrix-org/matrix-react-sdk/pull/6043) + * Update space order field validity requirements to match msc update + [\#6042](https://github.com/matrix-org/matrix-react-sdk/pull/6042) + * Try putting room list handling behind a lock + [\#6024](https://github.com/matrix-org/matrix-react-sdk/pull/6024) + * Improve progress bar progression for smaller voice messages + [\#6035](https://github.com/matrix-org/matrix-react-sdk/pull/6035) + * Fix share space edge case where space is public but not invitable + [\#6039](https://github.com/matrix-org/matrix-react-sdk/pull/6039) + * Add missing 'rel' to image view download button + [\#6033](https://github.com/matrix-org/matrix-react-sdk/pull/6033) + * Improve visible waveform for voice messages + [\#6034](https://github.com/matrix-org/matrix-react-sdk/pull/6034) + * Fix roving tab index intercepting home/end in space create menu + [\#6040](https://github.com/matrix-org/matrix-react-sdk/pull/6040) + * Decorate room avatars with publicity in add existing to space flow + [\#6030](https://github.com/matrix-org/matrix-react-sdk/pull/6030) + * Improve Spaces "Just Me" wizard + [\#6025](https://github.com/matrix-org/matrix-react-sdk/pull/6025) + * Increase hover feedback on room sub list buttons + [\#6037](https://github.com/matrix-org/matrix-react-sdk/pull/6037) + * Show alternative button during space creation wizard if no rooms + [\#6029](https://github.com/matrix-org/matrix-react-sdk/pull/6029) + * Swap rotation buttons in the image viewer + [\#6032](https://github.com/matrix-org/matrix-react-sdk/pull/6032) + * Typo: initilisation -> initialisation + [\#5915](https://github.com/matrix-org/matrix-react-sdk/pull/5915) + * Save edited state of a message when switching rooms + [\#6001](https://github.com/matrix-org/matrix-react-sdk/pull/6001) + * Fix shield icon in Untrusted Device Dialog + [\#6022](https://github.com/matrix-org/matrix-react-sdk/pull/6022) + * Do not eagerly decrypt breadcrumb rooms + [\#6028](https://github.com/matrix-org/matrix-react-sdk/pull/6028) + * Update spaces.png + [\#6031](https://github.com/matrix-org/matrix-react-sdk/pull/6031) + * Encourage more diverse reactions to content + [\#6027](https://github.com/matrix-org/matrix-react-sdk/pull/6027) + * Wrap decodeURIComponent in try-catch to protect against malformed URIs + [\#6026](https://github.com/matrix-org/matrix-react-sdk/pull/6026) + * Iterate beta feedback dialog + [\#6021](https://github.com/matrix-org/matrix-react-sdk/pull/6021) + * Disable space fields whilst their form is busy + [\#6020](https://github.com/matrix-org/matrix-react-sdk/pull/6020) + * Add missing space on beta feedback dialog + [\#6018](https://github.com/matrix-org/matrix-react-sdk/pull/6018) + * Fix colours used for the back button in space create menu + [\#6017](https://github.com/matrix-org/matrix-react-sdk/pull/6017) + * Prioritise and reduce the amount of events decrypted on application startup + [\#5980](https://github.com/matrix-org/matrix-react-sdk/pull/5980) + * Linkify topics in space room directory results + [\#6015](https://github.com/matrix-org/matrix-react-sdk/pull/6015) + * Persistent space collapsed states + [\#5972](https://github.com/matrix-org/matrix-react-sdk/pull/5972) + * Catch another instance of unlabeled avatars. + [\#6010](https://github.com/matrix-org/matrix-react-sdk/pull/6010) + * Rescale and smooth voice message playback waveform to better match + expectation + [\#5996](https://github.com/matrix-org/matrix-react-sdk/pull/5996) + * Scale voice message clock with user's font size + [\#5993](https://github.com/matrix-org/matrix-react-sdk/pull/5993) + * Remove "in development" flag from voice messages + [\#5995](https://github.com/matrix-org/matrix-react-sdk/pull/5995) + * Support voice messages on Safari + [\#5989](https://github.com/matrix-org/matrix-react-sdk/pull/5989) + * Translations update from Weblate + [\#6011](https://github.com/matrix-org/matrix-react-sdk/pull/6011) + +Changes in [3.21.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0) (2021-05-17) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0-rc.1...v3.21.0) + +## Security notice + +matrix-react-sdk 3.21.0 fixes a low severity issue (GHSA-8796-gc9j-63rv) +related to file upload. When uploading a file, the local file preview can lead +to execution of scripts embedded in the uploaded file, but only after several +user interactions to open the preview in a separate tab. This only impacts the +local user while in the process of uploading. It cannot be exploited remotely +or by other users. Thanks to [Muhammad Zaid Ghifari](https://github.com/MR-ZHEEV) +for responsibly disclosing this via Matrix's Security Disclosure Policy. + +## All changes + + * Upgrade to JS SDK 11.0.0 + * [Release] Add missing space on beta feedback dialog + [\#6019](https://github.com/matrix-org/matrix-react-sdk/pull/6019) + * [Release] Add feedback mechanism for beta features, namely Spaces + [\#6013](https://github.com/matrix-org/matrix-react-sdk/pull/6013) + * Add feedback mechanism for beta features, namely Spaces + [\#6012](https://github.com/matrix-org/matrix-react-sdk/pull/6012) + +Changes in [3.21.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0-rc.1) (2021-05-11) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0...v3.21.0-rc.1) + + * Upgrade to JS SDK 11.0.0-rc.1 + * Add disclaimer about subspaces being experimental in add existing dialog + [\#5978](https://github.com/matrix-org/matrix-react-sdk/pull/5978) + * Spaces Beta release + [\#5933](https://github.com/matrix-org/matrix-react-sdk/pull/5933) + * Improve permissions error when adding new server to room directory + [\#6009](https://github.com/matrix-org/matrix-react-sdk/pull/6009) + * Allow user to progress through space creation & setup using Enter + [\#6006](https://github.com/matrix-org/matrix-react-sdk/pull/6006) + * Upgrade sanitize types + [\#6008](https://github.com/matrix-org/matrix-react-sdk/pull/6008) + * Upgrade `cheerio` and resolve type errors + [\#6007](https://github.com/matrix-org/matrix-react-sdk/pull/6007) + * Add slash commands support to edit message composer + [\#5865](https://github.com/matrix-org/matrix-react-sdk/pull/5865) + * Fix the two todays problem + [\#5940](https://github.com/matrix-org/matrix-react-sdk/pull/5940) + * Switch the Home Space out for an All rooms space + [\#5969](https://github.com/matrix-org/matrix-react-sdk/pull/5969) + * Show device ID in UserInfo when there is no device name + [\#5985](https://github.com/matrix-org/matrix-react-sdk/pull/5985) + * Switch back to release version of `sanitize-html` + [\#6005](https://github.com/matrix-org/matrix-react-sdk/pull/6005) + * Bump hosted-git-info from 2.8.8 to 2.8.9 + [\#5998](https://github.com/matrix-org/matrix-react-sdk/pull/5998) + * Don't use the event's metadata to calc the scale of an image + [\#5982](https://github.com/matrix-org/matrix-react-sdk/pull/5982) + * Adjust MIME type of upload confirmation if needed + [\#5981](https://github.com/matrix-org/matrix-react-sdk/pull/5981) + * Forbid redaction of encryption events + [\#5991](https://github.com/matrix-org/matrix-react-sdk/pull/5991) + * Fix voice message playback being squished up against send button + [\#5988](https://github.com/matrix-org/matrix-react-sdk/pull/5988) + * Improve style of notification badges on the space panel + [\#5983](https://github.com/matrix-org/matrix-react-sdk/pull/5983) + * Add dev dependency for parse5 typings + [\#5990](https://github.com/matrix-org/matrix-react-sdk/pull/5990) + * Iterate Spaces admin UX around room management + [\#5977](https://github.com/matrix-org/matrix-react-sdk/pull/5977) + * Guard all isSpaceRoom calls behind the labs flag + [\#5979](https://github.com/matrix-org/matrix-react-sdk/pull/5979) + * Bump lodash from 4.17.20 to 4.17.21 + [\#5986](https://github.com/matrix-org/matrix-react-sdk/pull/5986) + * Bump lodash from 4.17.19 to 4.17.21 in /test/end-to-end-tests + [\#5987](https://github.com/matrix-org/matrix-react-sdk/pull/5987) + * Bump ua-parser-js from 0.7.23 to 0.7.28 + [\#5984](https://github.com/matrix-org/matrix-react-sdk/pull/5984) + * Update visual style of plain files in the timeline + [\#5971](https://github.com/matrix-org/matrix-react-sdk/pull/5971) + * Support for multiple streams (not MSC3077) + [\#5833](https://github.com/matrix-org/matrix-react-sdk/pull/5833) + * Update space ordering behaviour to match updates in MSC + [\#5963](https://github.com/matrix-org/matrix-react-sdk/pull/5963) + * Improve performance of search all spaces and space switching + [\#5976](https://github.com/matrix-org/matrix-react-sdk/pull/5976) + * Update colours and sizing for voice messages + [\#5970](https://github.com/matrix-org/matrix-react-sdk/pull/5970) + * Update link to Android SDK + [\#5973](https://github.com/matrix-org/matrix-react-sdk/pull/5973) + * Add cleanup functions for image view + [\#5962](https://github.com/matrix-org/matrix-react-sdk/pull/5962) + * Add a note about sharing your IP in P2P calls + [\#5961](https://github.com/matrix-org/matrix-react-sdk/pull/5961) + * Only aggregate DM notifications on the Space Panel in the Home Space + [\#5968](https://github.com/matrix-org/matrix-react-sdk/pull/5968) + * Add retry mechanism and progress bar to add existing to space dialog + [\#5975](https://github.com/matrix-org/matrix-react-sdk/pull/5975) + * Warn on access token reveal + [\#5755](https://github.com/matrix-org/matrix-react-sdk/pull/5755) + * Fix newly joined room appearing under the wrong space + [\#5945](https://github.com/matrix-org/matrix-react-sdk/pull/5945) + * Early rendering for voice messages in the timeline + [\#5955](https://github.com/matrix-org/matrix-react-sdk/pull/5955) + * Calculate the real waveform in the Playback class for voice messages + [\#5956](https://github.com/matrix-org/matrix-react-sdk/pull/5956) + * Don't recurse on arrayFastResample + [\#5957](https://github.com/matrix-org/matrix-react-sdk/pull/5957) + * Support a dark theme for voice messages + [\#5958](https://github.com/matrix-org/matrix-react-sdk/pull/5958) + * Handle no/blocked microphones in voice messages + [\#5959](https://github.com/matrix-org/matrix-react-sdk/pull/5959) + +Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0) + + * Upgrade to JS SDK 10.1.0 + * [Release] Don't use the event's metadata to calc the scale of an image + [\#6004](https://github.com/matrix-org/matrix-react-sdk/pull/6004) + +Changes in [3.20.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0-rc.1) (2021-05-04) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0...v3.20.0-rc.1) + + * Upgrade to JS SDK 10.1.0-rc.1 + * Translations update from Weblate + [\#5966](https://github.com/matrix-org/matrix-react-sdk/pull/5966) + * Fix more space panel layout and hover behaviour issues + [\#5965](https://github.com/matrix-org/matrix-react-sdk/pull/5965) + * Fix edge case with space panel alignment with subspaces on ff + [\#5964](https://github.com/matrix-org/matrix-react-sdk/pull/5964) + * Fix saving room pill part to history + [\#5951](https://github.com/matrix-org/matrix-react-sdk/pull/5951) + * Generate room preview even when minimized + [\#5948](https://github.com/matrix-org/matrix-react-sdk/pull/5948) + * Another change from recovery passphrase to Security Phrase + [\#5934](https://github.com/matrix-org/matrix-react-sdk/pull/5934) + * Sort rooms in the add existing to space dialog based on recency + [\#5943](https://github.com/matrix-org/matrix-react-sdk/pull/5943) + * Inhibit sending RR when context switching to a room + [\#5944](https://github.com/matrix-org/matrix-react-sdk/pull/5944) + * Prevent room list keyboard handling from landing focus on hidden nodes + [\#5950](https://github.com/matrix-org/matrix-react-sdk/pull/5950) + * Make the text filter search all spaces instead of just the selected one + [\#5942](https://github.com/matrix-org/matrix-react-sdk/pull/5942) + * Enable indent rule and fix indent + [\#5931](https://github.com/matrix-org/matrix-react-sdk/pull/5931) + * Prevent peeking members from reacting + [\#5946](https://github.com/matrix-org/matrix-react-sdk/pull/5946) + * Disallow inline display maths + [\#5939](https://github.com/matrix-org/matrix-react-sdk/pull/5939) + * Space creation prompt user to add existing rooms for "Just Me" spaces + [\#5923](https://github.com/matrix-org/matrix-react-sdk/pull/5923) + * Add test coverage collection script + [\#5937](https://github.com/matrix-org/matrix-react-sdk/pull/5937) + * Fix joining room using via servers regression + [\#5936](https://github.com/matrix-org/matrix-react-sdk/pull/5936) + * Revert "Fixes the two Todays problem in Redaction" + [\#5938](https://github.com/matrix-org/matrix-react-sdk/pull/5938) + * Handle encoded matrix URLs + [\#5903](https://github.com/matrix-org/matrix-react-sdk/pull/5903) + * Render ignored users setting regardless of if there are any + [\#5860](https://github.com/matrix-org/matrix-react-sdk/pull/5860) + * Fix inserting trailing colon after mention/pill + [\#5830](https://github.com/matrix-org/matrix-react-sdk/pull/5830) + * Fixes the two Todays problem in Redaction + [\#5917](https://github.com/matrix-org/matrix-react-sdk/pull/5917) + * Fix page up/down scrolling only half a page + [\#5920](https://github.com/matrix-org/matrix-react-sdk/pull/5920) + * Voice messages: Composer controls + [\#5935](https://github.com/matrix-org/matrix-react-sdk/pull/5935) + * Support MSC3086 asserted identity + [\#5886](https://github.com/matrix-org/matrix-react-sdk/pull/5886) + * Handle possible edge case with getting stuck in "unsent messages" bar + [\#5930](https://github.com/matrix-org/matrix-react-sdk/pull/5930) + * Fix suggested rooms not showing up regression from room list optimisation + [\#5932](https://github.com/matrix-org/matrix-react-sdk/pull/5932) + * Broadcast language change to ElectronPlatform + [\#5913](https://github.com/matrix-org/matrix-react-sdk/pull/5913) + * Fix VoIP PIP frame color + [\#5701](https://github.com/matrix-org/matrix-react-sdk/pull/5701) + * Convert some Flow-typed files to TypeScript + [\#5912](https://github.com/matrix-org/matrix-react-sdk/pull/5912) + * Initial SpaceStore tests work + [\#5906](https://github.com/matrix-org/matrix-react-sdk/pull/5906) + * Fix issues with space hierarchy in layout and with incompatible servers + [\#5926](https://github.com/matrix-org/matrix-react-sdk/pull/5926) + * Scale all mxc thumbs using device pixel ratio for hidpi + [\#5928](https://github.com/matrix-org/matrix-react-sdk/pull/5928) + * Fix add existing to space dialog no longer showing rooms for public spaces + [\#5918](https://github.com/matrix-org/matrix-react-sdk/pull/5918) + * Disable spaces context switching for when exploring a space + [\#5924](https://github.com/matrix-org/matrix-react-sdk/pull/5924) + * Autofocus search box in the add existing to space dialog + [\#5921](https://github.com/matrix-org/matrix-react-sdk/pull/5921) + * Use label element in add existing to space dialog for easier hit target + [\#5922](https://github.com/matrix-org/matrix-react-sdk/pull/5922) + * Dynamic max and min zoom in the new ImageView + [\#5916](https://github.com/matrix-org/matrix-react-sdk/pull/5916) + * Improve message error states + [\#5897](https://github.com/matrix-org/matrix-react-sdk/pull/5897) + * Check for null room in `VisibilityProvider` + [\#5914](https://github.com/matrix-org/matrix-react-sdk/pull/5914) + * Add unit tests for various collection-based utility functions + [\#5910](https://github.com/matrix-org/matrix-react-sdk/pull/5910) + * Spaces visual fixes + [\#5909](https://github.com/matrix-org/matrix-react-sdk/pull/5909) + * Remove reliance on DOM API to generated message preview + [\#5908](https://github.com/matrix-org/matrix-react-sdk/pull/5908) + * Expand upon voice message event & include overall waveform + [\#5888](https://github.com/matrix-org/matrix-react-sdk/pull/5888) + * Use floats for image background opacity + [\#5905](https://github.com/matrix-org/matrix-react-sdk/pull/5905) + * Show invites to spaces at the top of the space panel + [\#5902](https://github.com/matrix-org/matrix-react-sdk/pull/5902) + * Improve edge cases with spaces context switching + [\#5899](https://github.com/matrix-org/matrix-react-sdk/pull/5899) + * Fix spaces notification dots wrongly including upgraded (hidden) rooms + [\#5900](https://github.com/matrix-org/matrix-react-sdk/pull/5900) + * Iterate the spaces face pile design + [\#5898](https://github.com/matrix-org/matrix-react-sdk/pull/5898) + * Fix alignment issue with nested spaces being cut off wrong + [\#5890](https://github.com/matrix-org/matrix-react-sdk/pull/5890) + Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0) diff --git a/__mocks__/empty.js b/__mocks__/empty.js new file mode 100644 index 0000000000..51fb4fe937 --- /dev/null +++ b/__mocks__/empty.js @@ -0,0 +1,2 @@ +// Yes, this is empty. +module.exports = {}; diff --git a/package.json b/package.json index be195e2e9e..13047b69cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.19.0", + "version": "3.22.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -58,7 +58,7 @@ "blueimp-canvas-to-blob": "^3.28.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", - "cheerio": "^1.0.0-rc.5", + "cheerio": "^1.0.0-rc.9", "classnames": "^2.2.6", "commonmark": "^0.29.3", "counterpart": "^0.18.6", @@ -80,7 +80,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.20", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^0.1.0-beta.13", + "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", @@ -97,7 +97,7 @@ "react-transition-group": "^4.4.1", "resize-observer-polyfill": "^1.5.1", "rfc4648": "^1.4.0", - "sanitize-html": "github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db", + "sanitize-html": "^2.3.2", "tar-js": "^0.3.0", "text-encoding-utf-8": "^1.0.2", "url": "^0.11.0", @@ -121,6 +121,7 @@ "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.12.10", "@babel/traverse": "^7.12.12", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "@peculiar/webcrypto": "^1.1.4", "@sinonjs/fake-timers": "^7.0.2", "@types/classnames": "^2.2.11", @@ -137,7 +138,7 @@ "@types/react": "^16.9", "@types/react-dom": "^16.9.10", "@types/react-transition-group": "^4.4.0", - "@types/sanitize-html": "^1.27.0", + "@types/sanitize-html": "^2.3.1", "@types/zxcvbn": "^4.4.0", "@typescript-eslint/eslint-plugin": "^4.14.0", "@typescript-eslint/parser": "^4.14.0", @@ -161,7 +162,6 @@ "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", - "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "react-test-renderer": "^16.14.0", "rimraf": "^3.0.2", "stylelint": "^13.9.0", @@ -186,7 +186,10 @@ ], "moduleNameMapper": { "\\.(gif|png|svg|ttf|woff2)$": "/__mocks__/imageMock.js", - "\\$webapp/i18n/languages.json": "/__mocks__/languages.json" + "\\$webapp/i18n/languages.json": "/__mocks__/languages.json", + "decoderWorker\\.min\\.js": "/__mocks__/empty.js", + "decoderWorker\\.min\\.wasm": "/__mocks__/empty.js", + "waveWorker\\.min\\.js": "/__mocks__/empty.js" }, "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" diff --git a/res/css/_common.scss b/res/css/_common.scss index d6f85edb86..b128a82442 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -45,6 +45,8 @@ html { N.B. Breaks things when we have legitimate horizontal overscroll */ height: 100%; overflow: hidden; + // Stop similar overscroll bounce in Firefox Nightly for macOS + overscroll-behavior: none; } body { @@ -289,6 +291,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_staticWrapper .mx_Dialog { z-index: 4010; + contain: content; } .mx_Dialog_background { diff --git a/res/css/_components.scss b/res/css/_components.scss index ec9592f3a1..418b8f51c9 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -54,6 +54,7 @@ @import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/avatars/_PulsedAvatar.scss"; @import "./views/avatars/_WidgetAvatar.scss"; +@import "./views/beta/_BetaCard.scss"; @import "./views/context_menus/_CallContextMenu.scss"; @import "./views/context_menus/_IconizedContextMenu.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; @@ -62,6 +63,7 @@ @import "./views/dialogs/_AddExistingToSpaceDialog.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; +@import "./views/dialogs/_BetaFeedbackDialog.scss"; @import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; @@ -96,6 +98,7 @@ @import "./views/dialogs/_SpaceSettingsDialog.scss"; @import "./views/dialogs/_TabbedIntegrationManagerDialog.scss"; @import "./views/dialogs/_TermsDialog.scss"; +@import "./views/dialogs/_UntrustedDeviceDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss"; @import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss"; @@ -176,6 +179,7 @@ @import "./views/messages/_common_CryptoEvent.scss"; @import "./views/right_panel/_BaseCard.scss"; @import "./views/right_panel/_EncryptionInfo.scss"; +@import "./views/right_panel/_PinnedMessagesCard.scss"; @import "./views/right_panel/_RoomSummaryCard.scss"; @import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_VerificationPanel.scss"; @@ -200,7 +204,6 @@ @import "./views/rooms/_NewRoomIntro.scss"; @import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_PinnedEventTile.scss"; -@import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss"; @@ -237,6 +240,7 @@ @import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss"; @import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_LabsUserSettingsTab.scss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss"; @import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss"; @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; diff --git a/res/css/structures/_ContextualMenu.scss b/res/css/structures/_ContextualMenu.scss index 658033339a..d7f2cb76e8 100644 --- a/res/css/structures/_ContextualMenu.scss +++ b/res/css/structures/_ContextualMenu.scss @@ -38,6 +38,7 @@ limitations under the License. position: absolute; font-size: $font-14px; z-index: 5001; + contain: content; } .mx_ContextualMenu_right { @@ -115,8 +116,3 @@ limitations under the License. border-top: 8px solid $menu-bg-color; border-right: 8px solid transparent; } - -.mx_ContextualMenu_spinner { - display: block; - margin: 0 auto; -} diff --git a/res/css/structures/_GroupFilterPanel.scss b/res/css/structures/_GroupFilterPanel.scss index e5a8ef6df2..444435dd57 100644 --- a/res/css/structures/_GroupFilterPanel.scss +++ b/res/css/structures/_GroupFilterPanel.scss @@ -56,6 +56,12 @@ limitations under the License. .mx_GroupFilterPanel .mx_TagTile { // opacity: 0.5; position: relative; + + .mx_BetaDot { + position: absolute; + right: -13px; + top: -11px; + } } .mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype { diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 7c3cd1c513..c7dd678c07 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -25,6 +25,7 @@ $roomListCollapsedWidth: 68px; // Create a row-based flexbox for the GroupFilterPanel and the room list display: flex; + contain: content; .mx_LeftPanel_GroupFilterPanelContainer { flex-grow: 0; @@ -70,6 +71,7 @@ $roomListCollapsedWidth: 68px; // aligned correctly. This is also a row-based flexbox. display: flex; align-items: center; + contain: content; &.mx_IndicatorScrollbar_leftOverflow { mask-image: linear-gradient(90deg, transparent, black 5%); diff --git a/res/css/structures/_MyGroups.scss b/res/css/structures/_MyGroups.scss index 73f1332cd0..9c0062b72d 100644 --- a/res/css/structures/_MyGroups.scss +++ b/res/css/structures/_MyGroups.scss @@ -17,6 +17,11 @@ limitations under the License. .mx_MyGroups { display: flex; flex-direction: column; + + .mx_BetaCard { + margin: 0 72px; + max-width: 760px; + } } .mx_MyGroups .mx_RoomHeader_simpleHeader { @@ -30,7 +35,7 @@ limitations under the License. flex-wrap: wrap; } -.mx_MyGroups > :not(.mx_RoomHeader) { +.mx_MyGroups > :not(.mx_RoomHeader):not(.mx_BetaCard) { max-width: 960px; margin: 40px; } diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 5515fe4060..52a2a68b6a 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -25,6 +25,7 @@ limitations under the License. padding: 4px 0; box-sizing: border-box; height: 100%; + contain: strict; .mx_RoomView_MessageList { padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above @@ -98,6 +99,48 @@ limitations under the License. mask-position: center; } +$dot-size: 8px; +$pulse-color: $pinned-unread-color; + +.mx_RightPanel_pinnedMessagesButton { + &::before { + mask-image: url('$(res)/img/element-icons/room/pin.svg'); + mask-position: center; + } + + .mx_RightPanel_pinnedMessagesButton_unreadIndicator { + position: absolute; + right: 0; + top: 0; + margin: 4px; + width: $dot-size; + height: $dot-size; + border-radius: 50%; + transform: scale(1); + background: rgba($pulse-color, 1); + box-shadow: 0 0 0 0 rgba($pulse-color, 1); + animation: mx_RightPanel_indicator_pulse 2s infinite; + animation-iteration-count: 1; + } +} + +@keyframes mx_RightPanel_indicator_pulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba($pulse-color, 0); + } + + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0); + } +} + .mx_RightPanel_headerButton_highlight { &::before { background-color: $accent-color !important; diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 89cb21b7a6..ec07500af5 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -61,6 +61,39 @@ limitations under the License. .mx_RoomDirectory_tableWrapper { overflow-y: auto; flex: 1 1 0; + + .mx_RoomDirectory_footer { + margin-top: 24px; + text-align: center; + + > h5 { + margin: 0; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $primary-fg-color; + } + + > p { + margin: 40px auto 60px; + font-size: $font-14px; + line-height: $font-20px; + color: $secondary-fg-color; + max-width: 464px; // easier reading + } + + > hr { + margin: 0; + border: none; + height: 1px; + background-color: $header-panel-bg-color; + } + + .mx_RoomDirectory_newRoom { + margin: 24px auto 0; + width: max-content; + } + } } .mx_RoomDirectory_table { @@ -138,11 +171,6 @@ limitations under the License. color: $settings-grey-fg-color; } -.mx_RoomDirectory_table tr { - padding-bottom: 10px; - cursor: pointer; -} - .mx_RoomDirectory .mx_RoomView_MessageList { padding: 0; } diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index cdbe47178d..0efa2d01a1 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -152,6 +152,7 @@ limitations under the License. flex: 1; display: flex; flex-direction: column; + contain: content; } .mx_RoomView_statusArea { @@ -237,6 +238,7 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; + will-change: width; transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s; width: 99%; opacity: 1; diff --git a/res/css/structures/_ScrollPanel.scss b/res/css/structures/_ScrollPanel.scss index a4e501b339..7b75c69e86 100644 --- a/res/css/structures/_ScrollPanel.scss +++ b/res/css/structures/_ScrollPanel.scss @@ -21,5 +21,8 @@ limitations under the License. display: flex; flex-direction: column; justify-content: flex-end; + + content-visibility: auto; + contain-intrinsic-size: 50px; } } diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index ff51e28b7b..4bc4af467c 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -93,6 +93,10 @@ $SpaceRoomViewInnerWidth: 428px; } } + &:not(.mx_SpaceRoomView_landing) .mx_SpaceFeedbackPrompt { + width: $SpaceRoomViewInnerWidth; + } + .mx_SpaceRoomView_buttons { display: block; margin-top: 44px; @@ -103,6 +107,10 @@ $SpaceRoomViewInnerWidth: 428px; padding: 8px 22px; margin-left: 16px; } + + input.mx_AccessibleButton { + border: none; // override default styles + } } .mx_Field { @@ -133,6 +141,44 @@ $SpaceRoomViewInnerWidth: 428px; box-sizing: border-box; box-shadow: 2px 15px 30px $dialog-shadow-color; border-radius: 8px; + position: relative; + + // XXX remove this when spaces leaves Beta + .mx_BetaCard_betaPill { + position: absolute; + right: 24px; + top: 32px; + } + // XXX remove this when spaces leaves Beta + .mx_SpaceRoomView_preview_spaceBetaPrompt { + font-weight: $font-semi-bold; + font-size: $font-14px; + line-height: $font-24px; + color: $primary-fg-color; + margin-top: 24px; + position: relative; + padding-left: 24px; + + .mx_AccessibleButton_kind_link { + display: inline; + padding: 0; + font-size: inherit; + line-height: inherit; + } + + &::before { + content: ""; + position: absolute; + height: $font-24px; + width: 20px; + left: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + background-color: $secondary-fg-color; + } + } .mx_SpaceRoomView_preview_inviter { display: flex; @@ -282,6 +328,7 @@ $SpaceRoomViewInnerWidth: 428px; font-size: $font-15px; margin-top: 12px; margin-bottom: 16px; + white-space: pre; } > hr { @@ -293,10 +340,19 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SearchBox { margin: 0 0 20px; } + + .mx_SpaceFeedbackPrompt { + margin-bottom: 16px; + + // hide the HR as we have our own + & + hr { + display: none; + } + } } .mx_SpaceRoomView_privateScope { - .mx_AccessibleButton { + > .mx_AccessibleButton { @mixin SpacePillButton; } @@ -310,6 +366,23 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_inviteTeammates { + // XXX remove this when spaces leaves Beta + .mx_SpaceRoomView_inviteTeammates_betaDisclaimer { + padding: 58px 16px 16px; + position: relative; + border-radius: 8px; + background-color: $header-panel-bg-color; + max-width: $SpaceRoomViewInnerWidth; + margin: 20px 0 30px; + box-sizing: border-box; + + .mx_BetaCard_betaPill { + position: absolute; + left: 16px; + top: 16px; + } + } + .mx_SpaceRoomView_inviteTeammates_buttons { color: $secondary-fg-color; margin-top: 28px; @@ -391,3 +464,66 @@ $SpaceRoomViewInnerWidth: 428px; } } } + +.mx_SpaceFeedbackPrompt { + margin-top: 18px; + margin-bottom: 12px; + + > hr { + border: none; + border-top: 1px solid $input-border-color; + margin-bottom: 12px; + } + + > div { + display: flex; + flex-direction: row; + font-size: $font-15px; + line-height: $font-24px; + + > span { + color: $secondary-fg-color; + position: relative; + padding-left: 32px; + font-size: inherit; + line-height: inherit; + margin-right: auto; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + height: 20px; + width: 20px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + .mx_AccessibleButton_kind_link { + color: $accent-color; + position: relative; + padding: 0 0 0 24px; + margin-left: 8px; + font-size: inherit; + line-height: inherit; + + &::before { + content: ''; + position: absolute; + left: 0; + height: 16px; + width: 16px; + background-color: $accent-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); + mask-position: center; + } + } + } +} diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss index 2631cbfb40..257b512579 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.scss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_DecoratedRoomAvatar, .mx_ExtraTile { position: relative; + contain: content; &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg'); diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss new file mode 100644 index 0000000000..3463a653fc --- /dev/null +++ b/res/css/views/beta/_BetaCard.scss @@ -0,0 +1,114 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_BetaCard { + margin-bottom: 20px; + padding: 24px; + background-color: $settings-profile-placeholder-bg-color; + border-radius: 8px; + display: flex; + box-sizing: border-box; + + > div { + .mx_BetaCard_title { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + color: $primary-fg-color; + margin: 4px 0 14px; + + .mx_BetaCard_betaPill { + margin-left: 12px; + } + } + + .mx_BetaCard_caption { + font-size: $font-15px; + line-height: $font-20px; + color: $secondary-fg-color; + margin-bottom: 20px; + } + + .mx_AccessibleButton { + display: block; + margin: 12px 0; + padding: 7px 40px; + width: auto; + } + + .mx_BetaCard_disclaimer { + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + margin-top: 20px; + } + } + + > img { + margin: auto 0 auto 20px; + width: 300px; + object-fit: contain; + height: 100%; + } +} + +.mx_BetaCard_betaPill { + background-color: $accent-color-alt; + padding: 4px 10px; + border-radius: 8px; + text-transform: uppercase; + font-size: 12px; + line-height: 15px; + color: #FFFFFF; + display: inline-block; + vertical-align: text-bottom; + + &.mx_BetaCard_betaPill_clickable { + cursor: pointer; + } +} + +$pulse-color: $accent-color-alt; +$dot-size: 12px; + +.mx_BetaDot { + border-radius: 50%; + margin: 10px; + height: $dot-size; + width: $dot-size; + transform: scale(1); + background: rgba($pulse-color, 1); + box-shadow: 0 0 0 0 rgba($pulse-color, 1); + animation: mx_Beta_bluePulse 2s infinite; + animation-iteration-count: 20; +} + +@keyframes mx_Beta_bluePulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba($pulse-color, 0); + } + + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0); + } +} diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 524f107165..2776c477fc 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -54,7 +54,8 @@ limitations under the License. display: flex; margin-top: 12px; - .mx_BaseAvatar { + // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling + .mx_DecoratedRoomAvatar { margin-right: 12px; } @@ -75,10 +76,124 @@ limitations under the License. } .mx_AddExistingToSpace_section_spaces { + .mx_BaseAvatar { + margin-right: 12px; + } + .mx_BaseAvatar_image { border-radius: 8px; } } + + .mx_AddExistingToSpace_section_experimental { + position: relative; + border-radius: 8px; + margin: 12px 0; + padding: 8px 8px 8px 42px; + background-color: $header-panel-bg-color; + + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + .mx_AddExistingToSpace_footer { + display: flex; + margin-top: 20px; + + > span { + flex-grow: 1; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + .mx_ProgressBar { + height: 8px; + width: 100%; + + @mixin ProgressBarBorderRadius 8px; + } + + .mx_AddExistingToSpace_progressText { + margin-top: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + } + + > * { + vertical-align: middle; + } + } + + .mx_AddExistingToSpace_error { + padding-left: 12px; + + > img { + align-self: center; + } + + .mx_AddExistingToSpace_errorHeading { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $notice-primary-color; + } + + .mx_AddExistingToSpace_errorCaption { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $primary-fg-color; + } + } + + .mx_AccessibleButton { + display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + padding: 8px 36px; + } + + .mx_AddExistingToSpace_retryButton { + margin-left: 12px; + padding-left: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + background-color: $primary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + left: 0; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + } } .mx_AddExistingToSpaceDialog { @@ -163,88 +278,4 @@ limitations under the License. .mx_AddExistingToSpace { display: contents; } - - .mx_AddExistingToSpaceDialog_footer { - display: flex; - margin-top: 20px; - - > span { - flex-grow: 1; - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-fg-color; - - .mx_ProgressBar { - height: 8px; - width: 100%; - - @mixin ProgressBarBorderRadius 8px; - } - - .mx_AddExistingToSpaceDialog_progressText { - margin-top: 8px; - font-size: $font-15px; - line-height: $font-24px; - color: $primary-fg-color; - } - - > * { - vertical-align: middle; - } - } - - .mx_AddExistingToSpaceDialog_error { - padding-left: 12px; - - > img { - align-self: center; - } - - .mx_AddExistingToSpaceDialog_errorHeading { - font-weight: $font-semi-bold; - font-size: $font-15px; - line-height: $font-18px; - color: $notice-primary-color; - } - - .mx_AddExistingToSpaceDialog_errorCaption { - margin-top: 4px; - font-size: $font-12px; - line-height: $font-15px; - color: $primary-fg-color; - } - } - - .mx_AccessibleButton { - display: inline-block; - align-self: center; - } - - .mx_AccessibleButton_kind_primary { - padding: 8px 36px; - } - - .mx_AddExistingToSpaceDialog_retryButton { - margin-left: 12px; - padding-left: 24px; - position: relative; - - &::before { - content: ''; - position: absolute; - background-color: $primary-fg-color; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/retry.svg'); - width: 18px; - height: 18px; - left: 0; - } - } - - .mx_AccessibleButton_kind_link { - padding: 0; - } - } } diff --git a/res/css/views/rooms/_PinnedEventsPanel.scss b/res/css/views/dialogs/_BetaFeedbackDialog.scss similarity index 57% rename from res/css/views/rooms/_PinnedEventsPanel.scss rename to res/css/views/dialogs/_BetaFeedbackDialog.scss index 663d5bdf6e..9f5f6b512e 100644 --- a/res/css/views/rooms/_PinnedEventsPanel.scss +++ b/res/css/views/dialogs/_BetaFeedbackDialog.scss @@ -1,5 +1,5 @@ /* -Copyright 2017 Travis Ralston +Copyright 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,24 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_PinnedEventsPanel { - border-top: 1px solid $primary-hairline-color; -} +.mx_BetaFeedbackDialog { + .mx_BetaFeedbackDialog_subheading { + color: $primary-fg-color; + font-size: $font-14px; + line-height: $font-20px; + margin-bottom: 24px; + } -.mx_PinnedEventsPanel_body { - max-height: 300px; - overflow-y: auto; - padding-bottom: 15px; -} - -.mx_PinnedEventsPanel_header { - margin: 0; - padding-top: 8px; - padding-bottom: 15px; -} - -.mx_PinnedEventsPanel_cancel { - margin: 12px; - float: right; - display: inline-block; + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + line-height: inherit; + } } diff --git a/res/css/views/dialogs/_UntrustedDeviceDialog.scss b/res/css/views/dialogs/_UntrustedDeviceDialog.scss new file mode 100644 index 0000000000..0ecd9d4f71 --- /dev/null +++ b/res/css/views/dialogs/_UntrustedDeviceDialog.scss @@ -0,0 +1,26 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_UntrustedDeviceDialog { + .mx_Dialog_title { + display: flex; + align-items: center; + + .mx_E2EIcon { + margin-left: 0; + } + } +} diff --git a/res/css/views/elements/_InlineSpinner.scss b/res/css/views/elements/_InlineSpinner.scss index 6b91e45923..ca5cb5d3a8 100644 --- a/res/css/views/elements/_InlineSpinner.scss +++ b/res/css/views/elements/_InlineSpinner.scss @@ -18,7 +18,11 @@ limitations under the License. display: inline; } -.mx_InlineSpinner_spin img { +.mx_InlineSpinner img, .mx_InlineSpinner_icon { margin: 0px 6px; vertical-align: -3px; } + +.mx_InlineSpinner_icon { + display: inline-block; +} diff --git a/res/css/views/elements/_MiniAvatarUploader.scss b/res/css/views/elements/_MiniAvatarUploader.scss index 698184a095..df4676ab56 100644 --- a/res/css/views/elements/_MiniAvatarUploader.scss +++ b/res/css/views/elements/_MiniAvatarUploader.scss @@ -28,8 +28,7 @@ limitations under the License. top: 0; } - &::before, &::after { - content: ''; + .mx_MiniAvatarUploader_indicator { position: absolute; height: 26px; @@ -37,27 +36,22 @@ limitations under the License. right: -6px; bottom: -6px; - } - &::before { background-color: $primary-bg-color; border-radius: 50%; z-index: 1; - } - &::after { - background-color: $secondary-fg-color; - mask-position: center; - mask-repeat: no-repeat; - mask-image: url('$(res)/img/element-icons/camera.svg'); - mask-size: 16px; - z-index: 2; - } + .mx_MiniAvatarUploader_cameraIcon { + height: 100%; + width: 100%; - &.mx_MiniAvatarUploader_busy::after { - background: url("$(res)/img/spinner.gif") no-repeat center; - background-size: 80%; - mask: unset; + background-color: $secondary-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/camera.svg'); + mask-size: 16px; + z-index: 2; + } } } diff --git a/res/css/views/elements/_Spinner.scss b/res/css/views/elements/_Spinner.scss index 01b4f23c2c..93d5e2d96c 100644 --- a/res/css/views/elements/_Spinner.scss +++ b/res/css/views/elements/_Spinner.scss @@ -26,3 +26,19 @@ limitations under the License. .mx_MatrixChat_middlePanel .mx_Spinner { height: auto; } + +@keyframes spin { + from { + transform: rotateZ(0deg); + } + to { + transform: rotateZ(360deg); + } +} + +.mx_Spinner_icon { + background-color: $primary-fg-color; + mask: url('$(res)/img/spinner.svg'); + mask-size: contain; + animation: 1.1s steps(12, end) infinite spin; +} diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 3ecbef0d1f..e2fafe6c62 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -20,11 +20,12 @@ limitations under the License. visibility: hidden; cursor: pointer; display: flex; - height: 24px; + height: 32px; line-height: $font-24px; - border-radius: 4px; - background: $message-action-bar-bg-color; - top: -26px; + border-radius: 8px; + background: $primary-bg-color; + border: 1px solid $input-border-color; + top: -32px; right: 8px; user-select: none; // Ensure the action bar appears above over things, like the read marker. @@ -51,31 +52,19 @@ limitations under the License. white-space: nowrap; display: inline-block; position: relative; - border: 1px solid $message-action-bar-border-color; - margin-left: -1px; + margin: 2px; &:hover { - border-color: $message-action-bar-hover-border-color; + background: $roomlist-button-bg-color; + border-radius: 6px; z-index: 1; } - - &:first-child { - border-radius: 3px 0 0 3px; - } - - &:last-child { - border-radius: 0 3px 3px 0; - } - - &:only-child { - border-radius: 3px; - } } } - .mx_MessageActionBar_maskButton { - width: 27px; + width: 28px; + height: 28px; } .mx_MessageActionBar_maskButton::after { @@ -85,9 +74,14 @@ limitations under the License. left: 0; height: 100%; width: 100%; + mask-size: 18px; mask-repeat: no-repeat; mask-position: center; - background-color: $message-action-bar-fg-color; + background-color: $secondary-fg-color; +} + +.mx_MessageActionBar_maskButton:hover::after { + background-color: $primary-fg-color; } .mx_MessageActionBar_reactButton::after { diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index 2f5695e1fb..e05065eb02 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -17,18 +17,56 @@ limitations under the License. .mx_ReactionsRow { margin: 6px 0; color: $primary-fg-color; + + .mx_ReactionsRow_addReactionButton { + position: relative; + display: inline-block; + visibility: hidden; // show on hover of the .mx_EventTile + width: 24px; + height: 24px; + vertical-align: middle; + margin-left: 4px; + + &::before { + content: ''; + position: absolute; + height: 100%; + width: 100%; + mask-size: 16px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $tertiary-fg-color; + mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg'); + } + + &.mx_ReactionsRow_addReactionButton_active { + visibility: visible; // keep showing whilst the context menu is shown + } + + &:hover, &.mx_ReactionsRow_addReactionButton_active { + &::before { + background-color: $primary-fg-color; + } + } + } +} + +.mx_EventTile:hover .mx_ReactionsRow_addReactionButton { + visibility: visible; } .mx_ReactionsRow_showAll { text-decoration: none; - font-size: $font-10px; - font-weight: 600; - margin-left: 6px; - vertical-align: top; + font-size: $font-12px; + line-height: $font-20px; + margin-left: 4px; + vertical-align: middle; - &:hover, - &:link, - &:visited { - color: $accent-color; + &:link, &:visited { + color: $tertiary-fg-color; + } + + &:hover { + color: $primary-fg-color; } } diff --git a/res/css/views/messages/_ReactionsRowButton.scss b/res/css/views/messages/_ReactionsRowButton.scss index c132fa5a0f..766fea2f8f 100644 --- a/res/css/views/messages/_ReactionsRowButton.scss +++ b/res/css/views/messages/_ReactionsRowButton.scss @@ -16,14 +16,15 @@ limitations under the License. .mx_ReactionsRowButton { display: inline-flex; - line-height: $font-21px; + line-height: $font-20px; margin-right: 6px; - padding: 0 6px; + padding: 1px 6px; border: 1px solid $reaction-row-button-border-color; border-radius: 10px; background-color: $reaction-row-button-bg-color; cursor: pointer; user-select: none; + vertical-align: middle; &:hover { border-color: $reaction-row-button-hover-border-color; diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss new file mode 100644 index 0000000000..b6b8238bed --- /dev/null +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -0,0 +1,35 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_PinnedMessagesCard { + padding-top: 0; + + .mx_BaseCard_header { + text-align: center; + margin-top: 0; + border-bottom: 1px solid $menu-border-color; + + > h2 { + font-weight: $font-semi-bold; + font-size: $font-18px; + margin: 8px 0; + } + + .mx_BaseCard_close { + margin-right: 6px; + } + } +} diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss index 36882f4e8b..dc7804d072 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.scss +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -36,6 +36,7 @@ limitations under the License. -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; + white-space: pre-wrap; } .mx_RoomSummaryCard_avatar { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 5d1dd04383..51d9e1cc9d 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -104,7 +104,7 @@ $left-gutter: 64px; .mx_EventTile_line, .mx_EventTile_reply { position: relative; padding-left: $left-gutter; - border-radius: 4px; + border-radius: 8px; } .mx_RoomView_timeline_rr_enabled, @@ -280,6 +280,7 @@ $left-gutter: 64px; height: $font-14px; width: $font-14px; + will-change: left, top; transition: left var(--transition-short) ease-out, top var(--transition-standard) ease-out; diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index b6b901757c..cf61ce569d 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -115,8 +115,7 @@ $irc-line-height: $font-18px; .mx_EventTile_line { .mx_EventTile_e2eIcon, .mx_TextualEvent, - .mx_MTextBody, - .mx_ReplyThread_wrapper_empty { + .mx_MTextBody { display: inline-block; } } @@ -177,16 +176,13 @@ $irc-line-height: $font-18px; .mx_SenderProfile_hover { background-color: $primary-bg-color; overflow: hidden; + display: flex; - > span { - display: flex; - - > .mx_SenderProfile_name { - overflow: hidden; - text-overflow: ellipsis; - min-width: var(--name-width); - text-align: end; - } + > .mx_SenderProfile_name { + overflow: hidden; + text-overflow: ellipsis; + min-width: var(--name-width); + text-align: end; } } diff --git a/res/css/views/rooms/_JumpToBottomButton.scss b/res/css/views/rooms/_JumpToBottomButton.scss index 6cb3b6bce9..a8dc2ce11c 100644 --- a/res/css/views/rooms/_JumpToBottomButton.scss +++ b/res/css/views/rooms/_JumpToBottomButton.scss @@ -52,6 +52,7 @@ limitations under the License. .mx_JumpToBottomButton_scrollDown { position: relative; + display: block; height: 38px; border-radius: 19px; box-sizing: border-box; diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss index 9c2a428cb3..e0cccfa885 100644 --- a/res/css/views/rooms/_NewRoomIntro.scss +++ b/res/css/views/rooms/_NewRoomIntro.scss @@ -18,8 +18,8 @@ limitations under the License. margin: 40px 0 48px 64px; .mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) { - &::before, &::after { - content: unset; + .mx_MiniAvatarUploader_indicator { + display: none; } } diff --git a/res/css/views/rooms/_PinnedEventTile.scss b/res/css/views/rooms/_PinnedEventTile.scss index 030a76674a..15b3c16faa 100644 --- a/res/css/views/rooms/_PinnedEventTile.scss +++ b/res/css/views/rooms/_PinnedEventTile.scss @@ -16,62 +16,91 @@ limitations under the License. .mx_PinnedEventTile { min-height: 40px; - margin-bottom: 5px; width: 100%; - border-radius: 5px; // for the hover -} + padding: 0 4px 12px; -.mx_PinnedEventTile:hover { - background-color: $event-selected-color; -} + display: grid; + grid-template-areas: + "avatar name remove" + "content content content" + "footer footer footer"; + grid-template-rows: max-content auto max-content; + grid-template-columns: 24px auto 24px; + grid-row-gap: 12px; + grid-column-gap: 8px; -.mx_PinnedEventTile .mx_PinnedEventTile_sender, -.mx_PinnedEventTile .mx_PinnedEventTile_timestamp { - color: #868686; - font-size: 0.8em; - vertical-align: top; - display: inline-block; - padding-bottom: 3px; -} + & + .mx_PinnedEventTile { + padding: 12px 4px; + border-top: 1px solid $menu-border-color; + } -.mx_PinnedEventTile .mx_PinnedEventTile_timestamp { - padding-left: 15px; - display: none; -} + .mx_PinnedEventTile_senderAvatar { + grid-area: avatar; + } -.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar { - float: left; - margin-right: 10px; -} + .mx_PinnedEventTile_sender { + grid-area: name; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-24px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } -.mx_PinnedEventTile_actions { - float: right; - margin-right: 10px; - display: none; -} + .mx_PinnedEventTile_unpinButton { + visibility: hidden; + grid-area: remove; + position: relative; + width: 24px; + height: 24px; + border-radius: 8px; -.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp { - display: inline-block; -} + &:hover { + background-color: $roomheader-addroom-bg-color; + } -.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions { - display: block; -} + &::before { + content: ""; + position: absolute; + //top: 0; + //left: 0; + height: inherit; + width: inherit; + background: $secondary-fg-color; + mask-position: center; + mask-size: 8px; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/image-view/close.svg'); + } + } -.mx_PinnedEventTile_unpinButton { - display: inline-block; - cursor: pointer; - margin-left: 10px; -} + .mx_PinnedEventTile_message { + grid-area: content; + } -.mx_PinnedEventTile_gotoButton { - display: inline-block; - font-size: 0.7em; // Smaller text to avoid conflicting with the layout -} + .mx_PinnedEventTile_footer { + grid-area: footer; + font-size: 10px; + line-height: 12px; -.mx_PinnedEventTile_message { - margin-left: 50px; - position: relative; - top: 0; - left: 0; + .mx_PinnedEventTile_timestamp { + font-size: inherit; + line-height: inherit; + color: $secondary-fg-color; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + margin-left: 12px; + font-size: inherit; + line-height: inherit; + } + } + + &:hover { + .mx_PinnedEventTile_unpinButton { + visibility: visible; + } + } } diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss index 6512797401..152b0a45cd 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs.scss @@ -32,14 +32,14 @@ limitations under the License. // first triggering the enter state with the newest breadcrumb off screen (-40px) then // sliding it into view. &.mx_RoomBreadcrumbs-enter { - margin-left: -40px; // 32px for the avatar, 8px for the margin + transform: translateX(-40px); // 32px for the avatar, 8px for the margin } &.mx_RoomBreadcrumbs-enter-active { - margin-left: 0; + transform: translateX(0); // Timing function is as-requested by design. // NOTE: The transition time MUST match the value passed to CSSTransition! - transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1); + transition: transform 640ms cubic-bezier(0.66, 0.02, 0.36, 1); } .mx_RoomBreadcrumbs_placeholder { diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 387d1588a3..4142b0a2ef 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -277,24 +277,6 @@ limitations under the License. margin-top: 18px; } -.mx_RoomHeader_pinnedButton::before { - mask-image: url('$(res)/img/element-icons/room/pin.svg'); -} - -.mx_RoomHeader_pinsIndicator { - position: absolute; - right: 0; - bottom: 4px; - width: 8px; - height: 8px; - border-radius: 8px; - background-color: $pinned-color; -} - -.mx_RoomHeader_pinsIndicatorUnread { - background-color: $pinned-unread-color; -} - @media only screen and (max-width: 480px) { .mx_RoomHeader_wrapper { padding: 0; diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index 9d52e40819..146b3edf71 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -61,8 +61,8 @@ limitations under the License. &.mx_RoomSublist_headerContainer_sticky { position: fixed; height: 32px; // to match the header container - // width set by JS - width: calc(100% - 22px); + // width set by JS because of a compat issue between Firefox and Chrome + width: calc(100% - 15px); } // We don't have a top style because the top is dependent on the room list header's @@ -98,7 +98,7 @@ limitations under the License. position: relative; width: 24px; height: 24px; - border-radius: 32px; + border-radius: 8px; &::before { content: ''; @@ -114,6 +114,11 @@ limitations under the License. } } + .mx_RoomSublist_auxButton:hover, + .mx_RoomSublist_menuButton:hover { + background: $roomlist-button-bg-color; + } + // Hide the menu button by default .mx_RoomSublist_menuButton { visibility: hidden; @@ -193,6 +198,7 @@ limitations under the License. // as the box model should be top aligned. Happens in both FF and Chromium display: flex; flex-direction: column; + align-self: stretch; mask-image: linear-gradient(0deg, transparent, black 4px); } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 72d29dfd4c..03146e0325 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -19,6 +19,10 @@ limitations under the License. margin-bottom: 4px; padding: 4px; + contain: content; // Not strict as it will break when resizing a sublist vertically + height: 40px; + box-sizing: border-box; + // The tile is also a flexbox row itself display: flex; diff --git a/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss b/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss new file mode 100644 index 0000000000..540db48d65 --- /dev/null +++ b/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss @@ -0,0 +1,25 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_LabsUserSettingsTab { + .mx_SettingsTab_section { + margin-top: 32px; + + .mx_SettingsFlag { + margin-right: 0; // remove right margin to align with beta cards + } + } +} diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index ef3fea351b..88b9d8f693 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -29,6 +29,7 @@ $spacePanelWidth: 71px; width: 480px; box-sizing: border-box; background-color: $primary-bg-color; + position: relative; > div { > h2 { @@ -44,6 +45,13 @@ $spacePanelWidth: 71px; } } + // XXX remove this when spaces leaves Beta + .mx_BetaCard_betaPill { + position: absolute; + top: 24px; + right: 24px; + } + .mx_SpaceCreateMenuType { @mixin SpacePillButton; } @@ -59,7 +67,7 @@ $spacePanelWidth: 71px; width: 28px; height: 28px; position: relative; - background-color: $theme-button-bg-color; + background-color: $roomlist-button-bg-color; border-radius: 14px; margin-bottom: 12px; @@ -70,7 +78,7 @@ $spacePanelWidth: 71px; width: 28px; top: 0; left: 0; - background-color: $muted-fg-color; + background-color: $tertiary-fg-color; transform: rotate(90deg); mask-repeat: no-repeat; mask-position: 2px 3px; diff --git a/res/css/views/voice_messages/_PlaybackContainer.scss b/res/css/views/voice_messages/_PlaybackContainer.scss index 64e8f445e1..20def16d6a 100644 --- a/res/css/views/voice_messages/_PlaybackContainer.scss +++ b/res/css/views/voice_messages/_PlaybackContainer.scss @@ -46,7 +46,7 @@ limitations under the License. } .mx_Clock { - width: 42px; // we're not using a monospace font, so fake it + width: $font-42px; // we're not using a monospace font, so fake it padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended. padding-left: 8px; // isolate from recording circle / play control } diff --git a/res/img/betas/spaces.png b/res/img/betas/spaces.png new file mode 100644 index 0000000000..f4cfa90b4e Binary files /dev/null and b/res/img/betas/spaces.png differ diff --git a/res/img/element-icons/room/composer/emoji.svg b/res/img/element-icons/room/composer/emoji.svg index 9613d9edd9..b02cb69364 100644 --- a/res/img/element-icons/room/composer/emoji.svg +++ b/res/img/element-icons/room/composer/emoji.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/res/img/element-icons/room/message-bar/emoji.svg b/res/img/element-icons/room/message-bar/emoji.svg index 697f656b8a..07fee5b834 100644 --- a/res/img/element-icons/room/message-bar/emoji.svg +++ b/res/img/element-icons/room/message-bar/emoji.svg @@ -1,5 +1,3 @@ - - - - + + diff --git a/res/img/element-icons/room/pin.svg b/res/img/element-icons/room/pin.svg index 16941b329b..2448fc61c5 100644 --- a/res/img/element-icons/room/pin.svg +++ b/res/img/element-icons/room/pin.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/res/img/spinner.gif b/res/img/spinner.gif deleted file mode 100644 index ab4871214b..0000000000 Binary files a/res/img/spinner.gif and /dev/null differ diff --git a/res/img/spinner.svg b/res/img/spinner.svg index 08965e982e..c3680f19d2 100644 --- a/res/img/spinner.svg +++ b/res/img/spinner.svg @@ -1,141 +1,96 @@ - - - start - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index e8f2f1bc08..22280b8a28 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -42,6 +42,8 @@ import {SpaceStoreClass} from "../stores/SpaceStore"; import TypingStore from "../stores/TypingStore"; import { EventIndexPeg } from "../indexing/EventIndexPeg"; import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; +import PerformanceMonitor from "../performance"; +import UIStore from "../stores/UIStore"; declare global { interface Window { @@ -52,6 +54,9 @@ declare global { init: () => Promise; }; + // Needed for Safari, unknown to TypeScript + webkitAudioContext: typeof AudioContext; + mxContentMessages: ContentMessages; mxToastStore: ToastStore; mxDeviceListener: DeviceListener; @@ -76,6 +81,9 @@ declare global { mxVoiceRecordingStore: VoiceRecordingStore; mxTypingStore: TypingStore; mxEventIndexPeg: EventIndexPeg; + mxPerformanceMonitor: PerformanceMonitor; + mxPerformanceEntryNames: any; + mxUIStore: UIStore; } interface Document { diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 0268ebfe46..0d87451b5f 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -264,7 +264,7 @@ export default class CallHandler extends EventEmitter { } public getSupportsVirtualRooms() { - return this.supportsPstnProtocol; + return this.supportsSipNativeVirtual; } public pstnLookup(phoneNumber: string): Promise { @@ -462,6 +462,9 @@ export default class CallHandler extends EventEmitter { if (call.hangupReason === CallErrorCode.UserHangup) { title = _t("Call Declined"); description = _t("The other party declined the call."); + } else if (call.hangupReason === CallErrorCode.UserBusy) { + title = _t("User Busy"); + description = _t("The user you called is busy."); } else if (call.hangupReason === CallErrorCode.InviteTimeout) { title = _t("Call Failed"); // XXX: full stop appended as some relic here, but these @@ -518,7 +521,9 @@ export default class CallHandler extends EventEmitter { let newNativeAssertedIdentity = newAssertedIdentity; if (newAssertedIdentity) { const response = await this.sipNativeLookup(newAssertedIdentity); - if (response.length) newNativeAssertedIdentity = response[0].userid; + if (response.length && response[0].fields.lookup_success) { + newNativeAssertedIdentity = response[0].userid; + } } console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); @@ -799,7 +804,10 @@ export default class CallHandler extends EventEmitter { const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); if (this.getCallForRoom(mappedRoomId)) { - // ignore multiple incoming calls to the same room + console.log( + "Got incoming call for room " + mappedRoomId + + " but there's already a call for this room: ignoring", + ); return; } @@ -856,9 +864,43 @@ export default class CallHandler extends EventEmitter { }); break; } + case Action.DialNumber: + this.dialNumber(payload.number); + break; } } + private async dialNumber(number: string) { + const results = await this.pstnLookup(number); + if (!results || results.length === 0 || !results[0].userid) { + Modal.createTrackedDialog('', '', ErrorDialog, { + title: _t("Unable to look up phone number"), + description: _t("There was an error looking up the phone number"), + }); + return; + } + const userId = results[0].userid; + + // Now check to see if this is a virtual user, in which case we should find the + // native user + let nativeUserId; + if (this.getSupportsVirtualRooms()) { + const nativeLookupResults = await this.sipNativeLookup(userId); + const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; + nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; + console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); + } else { + nativeUserId = userId; + } + + const roomId = await ensureDMExists(MatrixClientPeg.get(), nativeUserId); + + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + } + setActiveCallRoomId(activeCallRoomId: string) { logger.info("Setting call in room " + activeCallRoomId + " active"); diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 95b45cce4a..b21829ac63 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -40,6 +40,7 @@ import { UploadStartedPayload, } from "./dispatcher/payloads/UploadPayload"; import {IUpload} from "./models/IUpload"; +import { IImageInfo } from "matrix-js-sdk/src/@types/partials"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -208,12 +209,12 @@ function infoForImageFile(matrixClient, roomId, imageFile) { } let imageInfo; - return loadImageElement(imageFile).then(function(r) { + return loadImageElement(imageFile).then((r) => { return createThumbnail(r.img, r.width, r.height, thumbnailType); - }).then(function(result) { + }).then((result) => { imageInfo = result.info; return uploadFile(matrixClient, roomId, result.thumbnail); - }).then(function(result) { + }).then((result) => { imageInfo.thumbnail_url = result.url; imageInfo.thumbnail_file = result.file; return imageInfo; @@ -264,12 +265,12 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { const thumbnailType = "image/jpeg"; let videoInfo; - return loadVideoElement(videoFile).then(function(video) { + return loadVideoElement(videoFile).then((video) => { return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType); - }).then(function(result) { + }).then((result) => { videoInfo = result.info; return uploadFile(matrixClient, roomId, result.thumbnail); - }).then(function(result) { + }).then((result) => { videoInfo.thumbnail_url = result.url; videoInfo.thumbnail_file = result.file; return videoInfo; @@ -308,7 +309,12 @@ function readFileAsArrayBuffer(file: File | Blob): Promise { * If the file is unencrypted then the object will have a "url" key. * If the file is encrypted then the object will have a "file" key. */ -function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) { +function uploadFile( + matrixClient: MatrixClient, + roomId: string, + file: File | Blob, + progressHandler?: any, // TODO: Types +): Promise<{url?: string, file?: any}> { // TODO: Types let canceled = false; if (matrixClient.isRoomEncrypted(roomId)) { // If the room is encrypted then encrypt the file before uploading it. @@ -355,7 +361,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo // If the attachment isn't encrypted then include the URL directly. return {"url": url}; }); - promise1.abort = () => { + (promise1 as any).abort = () => { canceled = true; MatrixClientPeg.get().cancelUpload(basePromise); }; @@ -367,7 +373,7 @@ export default class ContentMessages { private inprogress: IUpload[] = []; private mediaConfig: IMediaConfig = null; - sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) { + sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) { const startTime = CountlyAnalytics.getTimestamp(); const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); @@ -441,7 +447,7 @@ export default class ContentMessages { let uploadAll = false; // Promise to complete before sending next file into room, used for synchronisation of file-sending // to match the order the files were specified in - let promBefore = Promise.resolve(); + let promBefore: Promise = Promise.resolve(); for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index 974c08df18..5545ed8483 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -22,6 +22,7 @@ import SdkConfig from './SdkConfig'; import {MatrixClientPeg} from "./MatrixClientPeg"; import {sleep} from "./utils/promise"; import RoomViewStore from "./stores/RoomViewStore"; +import { Action } from "./dispatcher/actions"; // polyfill textencoder if necessary import * as TextEncodingUtf8 from 'text-encoding-utf-8'; @@ -265,7 +266,7 @@ interface ICreateRoomEvent extends IEvent { } interface IJoinRoomEvent extends IEvent { - key: "join_room"; + key: Action.JoinRoom; dur: number; // how long it took to join (until remote echo) segmentation: { room_id: string; // hashed @@ -684,7 +685,9 @@ export default class CountlyAnalytics { } private getOrientation = (): Orientation => { - return window.innerWidth > window.innerHeight ? Orientation.Landscape : Orientation.Portrait; + return window.matchMedia("(orientation: landscape)").matches + ? Orientation.Landscape + : Orientation.Portrait }; private reportOrientation = () => { @@ -813,7 +816,9 @@ export default class CountlyAnalytics { window.addEventListener("mousemove", this.onUserActivity); window.addEventListener("click", this.onUserActivity); window.addEventListener("keydown", this.onUserActivity); - window.addEventListener("scroll", this.onUserActivity); + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + window.addEventListener("scroll", this.onUserActivity, { passive: true }); this.activityIntervalId = setInterval(() => { this.inactivityCounter++; @@ -858,7 +863,7 @@ export default class CountlyAnalytics { } public trackRoomJoin(startTime: number, roomId: string, type: IJoinRoomEvent["segmentation"]["type"]) { - this.track("join_room", { type }, roomId, { + this.track(Action.JoinRoom, { type }, roomId, { dur: CountlyAnalytics.getTimestamp() - startTime, }); } diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index d956189f0d..9497d9de4c 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -21,7 +21,6 @@ import MultiInviter from './utils/MultiInviter'; import { _t } from './languageHandler'; import {MatrixClientPeg} from './MatrixClientPeg'; import GroupStore from './stores/GroupStore'; -import {allSettled} from "./utils/promise"; import StyledCheckbox from './components/views/elements/StyledCheckbox'; export function showGroupInviteDialog(groupId) { @@ -120,7 +119,7 @@ function _onGroupInviteFinished(groupId, addrs) { function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { const matrixClient = MatrixClientPeg.get(); const errorList = []; - return allSettled(addrs.map((addr) => { + return Promise.allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroup(groupId, addr.address, addRoomsPublicly) .catch(() => { errorList.push(addr.address); }) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 6b2568d68c..ef5ac383e3 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -422,8 +422,12 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts safeBody = sanitizeHtml(formattedBody, sanitizeParams); if (SettingsStore.getValue("feature_latex_maths")) { - const phtml = cheerio.load(safeBody, - { _useHtmlParser2: true, decodeEntities: false }) + const phtml = cheerio.load(safeBody, { + // @ts-ignore: The `_useHtmlParser2` internal option is the + // simplest way to both parse and render using `htmlparser2`. + _useHtmlParser2: true, + decodeEntities: false, + }); // @ts-ignore - The types for `replaceWith` wrongly expect // Cheerio instance to be returned. phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) { @@ -431,6 +435,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')), { throwOnError: false, + // @ts-ignore - `e` can be an Element, not just a Node displayMode: e.name == 'div', output: "htmlAndMathml", }); diff --git a/src/Login.ts b/src/Login.ts index d584df7dfe..7caab22d88 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -31,12 +31,12 @@ interface IPasswordFlow { } export enum IdentityProviderBrand { - Gitlab = "org.matrix.gitlab", - Github = "org.matrix.github", - Apple = "org.matrix.apple", - Google = "org.matrix.google", - Facebook = "org.matrix.facebook", - Twitter = "org.matrix.twitter", + Gitlab = "gitlab", + Github = "github", + Apple = "apple", + Google = "google", + Facebook = "facebook", + Twitter = "twitter", } export interface IIdentityProvider { @@ -48,7 +48,8 @@ export interface IIdentityProvider { export interface ISSOFlow { type: "m.login.sso" | "m.login.cas"; - "org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858 + // eslint-disable-next-line camelcase + identity_providers: IIdentityProvider[]; } export type LoginFlow = ISSOFlow | IPasswordFlow; diff --git a/src/Notifier.ts b/src/Notifier.ts index 3e927cea0c..4f55046e72 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -331,6 +331,8 @@ export const Notifier = { if (!this.isSyncing) return; // don't alert for any messages initially if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; + MatrixClientPeg.get().decryptEventIfNeeded(ev); + // If it's an encrypted event and the type is still 'm.room.encrypted', // it hasn't yet been decrypted, so wait until it is. if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { diff --git a/src/Presence.ts b/src/Presence.ts index eb56c5714e..8f2e127cb4 100644 --- a/src/Presence.ts +++ b/src/Presence.ts @@ -98,7 +98,7 @@ class Presence { } try { - await MatrixClientPeg.get().setPresence(this.state); + await MatrixClientPeg.get().setPresence({presence: this.state}); console.info("Presence:", newState); } catch (err) { console.error("Failed to set presence:", err); diff --git a/src/Searching.js b/src/Searching.js index f65b8920b3..2b17aee054 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -66,7 +66,7 @@ async function serverSideSearchProcess(term, roomId = undefined) { highlights: [], }; - return client._processRoomEventsSearch(searchResult, result.response); + return client.processRoomEventsSearch(searchResult, result.response); } function compareEvents(a, b) { @@ -131,7 +131,7 @@ async function combinedSearch(searchTerm) { }, }; - const result = client._processRoomEventsSearch(emptyResult, response); + const result = client.processRoomEventsSearch(emptyResult, response); // Restore our encryption info so we can properly re-verify the events. restoreEncryptionInfo(result.results); @@ -185,7 +185,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) { }, }; - const processedResult = MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); + const processedResult = MatrixClientPeg.get().processRoomEventsSearch(emptyResult, response); // Restore our encryption info so we can properly re-verify the events. restoreEncryptionInfo(processedResult.results); @@ -210,7 +210,7 @@ async function localPagination(searchResult) { }, }; - const result = MatrixClientPeg.get()._processRoomEventsSearch(searchResult, response); + const result = MatrixClientPeg.get().processRoomEventsSearch(searchResult, response); // Restore our encryption info so we can properly re-verify the events. const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0)); @@ -520,7 +520,7 @@ async function combinedPagination(searchResult) { const oldResultCount = searchResult.results ? searchResult.results.length : 0; // Let the client process the combined result. - const result = client._processRoomEventsSearch(searchResult, response); + const result = client.processRoomEventsSearch(searchResult, response); // Restore our encryption info so we can properly re-verify the events. const newResultCount = result.results.length - oldResultCount; diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 203830d232..09c8d30614 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -271,7 +271,7 @@ async function onSecretRequested( } return key && encodeBase64(key); } else if (name === "m.megolm_backup.v1") { - const key = await client._crypto.getSessionBackupPrivateKey(); + const key = await client.crypto.getSessionBackupPrivateKey(); if (!key) { console.log( `session backup key requested by ${deviceId}, but not found in cache`, diff --git a/src/Terms.ts b/src/Terms.ts index 1bdff36cbc..a6ea40a6e8 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -36,14 +36,18 @@ export class Service { } } -interface Policy { +export interface LocalisedPolicy { + name: string; + url: string; +} + +export interface Policy { // @ts-ignore: No great way to express indexed types together with other keys version: string; - [lang: string]: { - url: string; - }; + [lang: string]: LocalisedPolicy; } -type Policies = { + +export type Policies = { [policy: string]: Policy, }; @@ -99,7 +103,7 @@ export async function startTermsFlow( // fetch the set of agreed policy URLs from account data const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms'); - let agreedUrlSet; + let agreedUrlSet: Set; if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) { agreedUrlSet = new Set(); } else { diff --git a/src/Unread.js b/src/Unread.js index 12c15eb6af..25c425aa9a 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -40,6 +40,8 @@ export function eventTriggersUnreadCount(ev) { return false; } else if (ev.getType() == 'm.room.server_acl') { return false; + } else if (ev.isRedacted()) { + return false; } return haveTileForEvent(ev); } diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index e5bed2e812..d576a5434c 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -33,7 +33,7 @@ export default class VoipUserMapper { private async userToVirtualUser(userId: string): Promise { const results = await CallHandler.sharedInstance().sipVirtualLookup(userId); - if (results.length === 0) return null; + if (results.length === 0 || !results[0].fields.lookup_success) return null; return results[0].userid; } @@ -82,14 +82,14 @@ export default class VoipUserMapper { return Boolean(claimedNativeRoomId); } - public async onNewInvitedRoom(invitedRoom: Room) { + public async onNewInvitedRoom(invitedRoom: Room): Promise { if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return; const inviterId = invitedRoom.getDMInviter(); console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); if (result.length === 0) { - return true; + return; } if (result[0].fields.is_virtual) { diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index b49a90d175..4cb537f318 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -167,7 +167,7 @@ export const RovingTabIndexProvider: React.FC = ({children, handleHomeEn const onKeyDownHandler = useCallback((ev) => { let handled = false; // Don't interfere with input default keydown behaviour - if (handleHomeEnd && ev.target.tagName !== "INPUT") { + if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") { // check if we actually have any items switch (ev.key) { case Key.HOME: diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index a40ce7144d..2242fec914 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -93,7 +93,12 @@ export default class AutocompleteProvider { }; } - async getCompletions(query: string, selection: ISelectionRange, force = false): Promise { + async getCompletions( + query: string, + selection: ISelectionRange, + force = false, + limit = -1, + ): Promise { return []; } diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 2615736e09..5409825f45 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -26,6 +26,8 @@ import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; import {timeout} from "../utils/promise"; import AutocompleteProvider, {ICommand} from "./AutocompleteProvider"; +import SettingsStore from "../settings/SettingsStore"; +import SpaceProvider from "./SpaceProvider"; export interface ISelectionRange { beginning?: boolean; // whether the selection is in the first block of the editor or not @@ -56,6 +58,11 @@ const PROVIDERS = [ DuckDuckGoProvider, ]; +// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here +if (SettingsStore.getValue("feature_spaces")) { + PROVIDERS.push(SpaceProvider); +} + // Providers will get rejected if they take longer than this. const PROVIDER_COMPLETION_TIMEOUT = 3000; @@ -82,15 +89,24 @@ export default class Autocompleter { }); } - async getCompletions(query: string, selection: ISelectionRange, force = false): Promise { + async getCompletions( + query: string, + selection: ISelectionRange, + force = false, + limit = -1, + ): Promise { /* Note: This intentionally waits for all providers to return, otherwise, we run into a condition where new completions are displayed while the user is interacting with the list, which makes it difficult to predict whether an action will actually do what is intended */ // list of results from each provider, each being a list of completions or null if it times out - const completionsList: ICompletion[][] = await Promise.all(this.providers.map(provider => { - return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT); + const completionsList: ICompletion[][] = await Promise.all(this.providers.map(async provider => { + return await timeout( + provider.getCompletions(query, selection, force, limit), + null, + PROVIDER_COMPLETION_TIMEOUT, + ); })); // map then filter to maintain the index for the map-operation, for this.providers to line up diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index c2d1290e08..9de25c0d84 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -38,7 +38,12 @@ export default class CommandProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise { + async getCompletions( + query: string, + selection: ISelectionRange, + force?: boolean, + limit = -1, + ): Promise { const {command, range} = this.getCurrentCommand(query, selection); if (!command) return []; @@ -55,10 +60,11 @@ export default class CommandProvider extends AutocompleteProvider { } else { if (query === '/') { // If they have just entered `/` show everything + // We exclude the limit on purpose to have a comprehensive list matches = Commands; } else { // otherwise fuzzy match against all of the fields - matches = this.matcher.match(command[1]); + matches = this.matcher.match(command[1], limit); } } diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index b7a4e0960e..c9358b0c61 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -50,7 +50,12 @@ export default class CommunityProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: ISelectionRange, force = false): Promise { + async getCompletions( + query: string, + selection: ISelectionRange, + force = false, + limit = -1, + ): Promise { const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); // Disable autocompletions when composing commands because of various issues @@ -81,7 +86,7 @@ export default class CommunityProvider extends AutocompleteProvider { this.matcher.setObjects(groups); const matchedString = command[0]; - completions = this.matcher.match(matchedString); + completions = this.matcher.match(matchedString, limit); completions = sortBy(completions, [ (c) => score(matchedString, c.groupId), (c) => c.groupId.length, diff --git a/src/autocomplete/DuckDuckGoProvider.tsx b/src/autocomplete/DuckDuckGoProvider.tsx index e63f7255dc..3ef9cc2f6f 100644 --- a/src/autocomplete/DuckDuckGoProvider.tsx +++ b/src/autocomplete/DuckDuckGoProvider.tsx @@ -36,7 +36,12 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; } - async getCompletions(query: string, selection: ISelectionRange, force= false): Promise { + async getCompletions( + query: string, + selection: ISelectionRange, + force = false, + limit = -1, + ): Promise { const {command, range} = this.getCurrentCommand(query, selection); if (!query || !command) { return []; @@ -46,7 +51,8 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { method: 'GET', }); const json = await response.json(); - const results = json.Results.map((result) => { + const maxLength = limit > -1 ? limit : json.Results.length; + const results = json.Results.slice(0, maxLength).map((result) => { return { completion: result.Text, component: ( diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 705474f8d0..b7c4a5120a 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -84,7 +84,12 @@ export default class EmojiProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise { + async getCompletions( + query: string, + selection: ISelectionRange, + force?: boolean, + limit = -1, + ): Promise { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } @@ -93,7 +98,7 @@ export default class EmojiProvider extends AutocompleteProvider { const {command, range} = this.getCurrentCommand(query, selection); if (command) { const matchedString = command[0]; - completions = this.matcher.match(matchedString); + completions = this.matcher.match(matchedString, limit); // Do second match with shouldMatchWordsOnly in order to match against 'name' completions = completions.concat(this.nameMatcher.match(matchedString)); diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index ef1823c0ca..0bc7ead097 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -33,7 +33,12 @@ export default class NotifProvider extends AutocompleteProvider { this.room = room; } - async getCompletions(query: string, selection: ISelectionRange, force= false): Promise { + async getCompletions( + query: string, + selection: ISelectionRange, + force = false, + limit = -1, + ): Promise { const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); const client = MatrixClientPeg.get(); diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 91fbea4d6a..73bb37ff0f 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -21,7 +21,7 @@ import {removeHiddenChars} from "matrix-js-sdk/src/utils"; interface IOptions { keys: Array; - funcs?: Array<(T) => string>; + funcs?: Array<(T) => string | string[]>; shouldMatchWordsOnly?: boolean; // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true fuzzy?: boolean; @@ -69,7 +69,12 @@ export default class QueryMatcher { if (this._options.funcs) { for (const f of this._options.funcs) { - keyValues.push(f(object)); + const v = f(object); + if (Array.isArray(v)) { + keyValues.push(...v); + } else { + keyValues.push(v); + } } } @@ -87,7 +92,7 @@ export default class QueryMatcher { } } - match(query: string): T[] { + match(query: string, limit = -1): T[] { query = this.processQuery(query); if (this._options.shouldMatchWordsOnly) { query = query.replace(/[^\w]/g, ''); @@ -129,7 +134,10 @@ export default class QueryMatcher { }); // Now map the keys to the result objects. Also remove any duplicates. - return uniq(matches.map((match) => match.object)); + const dedupped = uniq(matches.map((match) => match.object)); + const maxLength = limit === -1 ? dedupped.length : limit; + + return dedupped.slice(0, maxLength); } private processQuery(query: string): string { diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 74deacf61f..ad55b19101 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -1,8 +1,7 @@ /* Copyright 2016 Aviral Dasgupta -Copyright 2017 Vector Creations Ltd -Copyright 2017, 2018 New Vector Ltd Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2017, 2018, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,17 +16,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; +import {uniqBy, sortBy} from "lodash"; import Room from "matrix-js-sdk/src/models/room"; + import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import {MatrixClientPeg} from '../MatrixClientPeg'; import QueryMatcher from './QueryMatcher'; import {PillCompletion} from './Components'; -import * as sdk from '../index'; import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; -import {uniqBy, sortBy} from "lodash"; +import RoomAvatar from '../components/views/avatars/RoomAvatar'; +import SettingsStore from "../settings/SettingsStore"; const ROOM_REGEX = /\B#\S*/g; @@ -49,7 +50,7 @@ function matcherObject(room: Room, displayedAlias: string, matchName = "") { } export default class RoomProvider extends AutocompleteProvider { - matcher: QueryMatcher; + protected matcher: QueryMatcher; constructor() { super(ROOM_REGEX); @@ -58,15 +59,28 @@ export default class RoomProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: ISelectionRange, force = false): Promise { - const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); + protected getRooms() { + const cli = MatrixClientPeg.get(); + let rooms = cli.getVisibleRooms(); - const client = MatrixClientPeg.get(); + if (SettingsStore.getValue("feature_spaces")) { + rooms = rooms.filter(r => !r.isSpaceRoom()); + } + + return rooms; + } + + async getCompletions( + query: string, + selection: ISelectionRange, + force = false, + limit = -1, + ): Promise { let completions = []; const {command, range} = this.getCurrentCommand(query, selection, force); if (command) { // the only reason we need to do this is because Fuse only matches on properties - let matcherObjects = client.getVisibleRooms().reduce((aliases, room) => { + let matcherObjects = this.getRooms().reduce((aliases, room) => { if (room.getCanonicalAlias()) { aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name)); } @@ -90,7 +104,7 @@ export default class RoomProvider extends AutocompleteProvider { this.matcher.setObjects(matcherObjects); const matchedString = command[0]; - completions = this.matcher.match(matchedString); + completions = this.matcher.match(matchedString, limit); completions = sortBy(completions, [ (c) => score(matchedString, c.displayedAlias), (c) => c.displayedAlias.length, @@ -110,7 +124,7 @@ export default class RoomProvider extends AutocompleteProvider { ), range, }; - }).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4); + }).filter((completion) => !!completion.completion && completion.completion.length > 0); } return completions; } diff --git a/src/autocomplete/SpaceProvider.tsx b/src/autocomplete/SpaceProvider.tsx new file mode 100644 index 0000000000..0361a2c91e --- /dev/null +++ b/src/autocomplete/SpaceProvider.tsx @@ -0,0 +1,43 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import { _t } from '../languageHandler'; +import {MatrixClientPeg} from '../MatrixClientPeg'; +import RoomProvider from "./RoomProvider"; + +export default class SpaceProvider extends RoomProvider { + protected getRooms() { + return MatrixClientPeg.get().getVisibleRooms().filter(r => r.isSpaceRoom()); + } + + getName() { + return _t("Spaces"); + } + + renderCompletions(completions: React.ReactNode[]): React.ReactNode { + return ( +
+ { completions } +
+ ); + } +} diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 5f0cfc2df1..3cf43d0b84 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -102,7 +102,12 @@ export default class UserProvider extends AutocompleteProvider { this.users = null; }; - async getCompletions(rawQuery: string, selection: ISelectionRange, force = false): Promise { + async getCompletions( + rawQuery: string, + selection: ISelectionRange, + force = false, + limit = -1, + ): Promise { const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); // lazy-load user list into matcher @@ -118,7 +123,7 @@ export default class UserProvider extends AutocompleteProvider { if (fullMatch && fullMatch !== '@') { // Don't include the '@' in our search query - it's only used as a way to trigger completion const query = fullMatch.startsWith('@') ? fullMatch.substring(1) : fullMatch; - completions = this.matcher.match(query).map((user) => { + completions = this.matcher.match(query, limit).map((user) => { const displayName = (user.name || user.userId || ''); return { // Length of completion should equal length of text in decorator. draft-js diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js deleted file mode 100644 index 14f7c9ca83..0000000000 --- a/src/components/structures/AutoHideScrollbar.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2018 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; - -export default class AutoHideScrollbar extends React.Component { - constructor(props) { - super(props); - this._collectContainerRef = this._collectContainerRef.bind(this); - } - - _collectContainerRef(ref) { - if (ref && !this.containerRef) { - this.containerRef = ref; - } - if (this.props.wrappedRef) { - this.props.wrappedRef(ref); - } - } - - getScrollTop() { - return this.containerRef.scrollTop; - } - - render() { - return (
- { this.props.children } -
); - } -} diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx new file mode 100644 index 0000000000..66f998b616 --- /dev/null +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -0,0 +1,65 @@ +/* +Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +interface IProps { + className?: string; + onScroll?: () => void; + onWheel?: () => void; + style?: React.CSSProperties + tabIndex?: number, + wrappedRef?: (ref: HTMLDivElement) => void; +} + +export default class AutoHideScrollbar extends React.Component { + private containerRef: React.RefObject = React.createRef(); + + public componentDidMount() { + if (this.containerRef.current && this.props.onScroll) { + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true }); + } + + if (this.props.wrappedRef) { + this.props.wrappedRef(this.containerRef.current); + } + } + + public componentWillUnmount() { + if (this.containerRef.current && this.props.onScroll) { + this.containerRef.current.removeEventListener("scroll", this.props.onScroll); + } + } + + public getScrollTop(): number { + return this.containerRef.current.scrollTop; + } + + public render() { + return (
+ { this.props.children } +
); + } +} diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 9d9d57d8a6..9d8665c176 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -23,6 +23,7 @@ import classNames from "classnames"; import {Key} from "../../Keyboard"; import {Writeable} from "../../@types/common"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import UIStore from "../../stores/UIStore"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -222,10 +223,12 @@ export class ContextMenu extends React.PureComponent { }; private onKeyDown = (ev: React.KeyboardEvent) => { + // don't let keyboard handling escape the context menu + ev.stopPropagation(); + if (!this.props.managed) { if (ev.key === Key.ESCAPE) { this.props.onFinished(); - ev.stopPropagation(); ev.preventDefault(); } return; @@ -258,7 +261,6 @@ export class ContextMenu extends React.PureComponent { if (handled) { // consume all other keys in context menu - ev.stopPropagation(); ev.preventDefault(); } }; @@ -409,12 +411,12 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None const buttonBottom = elementRect.bottom + window.pageYOffset; const buttonTop = elementRect.top + window.pageYOffset; // Align the right edge of the menu to the right edge of the button - menuOptions.right = window.innerWidth - buttonRight; + menuOptions.right = UIStore.instance.windowWidth - buttonRight; // Align the menu vertically on whichever side of the button has more space available. - if (buttonBottom < window.innerHeight / 2) { + if (buttonBottom < UIStore.instance.windowHeight / 2) { menuOptions.top = buttonBottom + vPadding; } else { - menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding; + menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding; } return menuOptions; @@ -429,12 +431,12 @@ export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFac const buttonBottom = elementRect.bottom + window.pageYOffset; const buttonTop = elementRect.top + window.pageYOffset; // Align the right edge of the menu to the right edge of the button - menuOptions.right = window.innerWidth - buttonRight; + menuOptions.right = UIStore.instance.windowWidth - buttonRight; // Align the menu vertically on whichever side of the button has more space available. - if (buttonBottom < window.innerHeight / 2) { + if (buttonBottom < UIStore.instance.windowHeight / 2) { menuOptions.top = buttonBottom + vPadding; } else { - menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding; + menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding; } return menuOptions; @@ -450,7 +452,7 @@ export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFa // Align the left edge of the menu to the left edge of the button menuOptions.left = buttonLeft; // Align the menu vertically above the menu - menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding; + menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding; return menuOptions; }; diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index d5e4b092e2..bb7c1f9642 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -50,6 +50,9 @@ class FilePanel extends React.Component { if (room?.roomId !== this.props?.roomId) return; if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; + const client = MatrixClientPeg.get(); + client.decryptEventIfNeeded(ev); + if (ev.isBeingDecrypted()) { this.decryptingEvents.add(ev.getId()); } else { diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js index 7c050e7433..2ff91e4976 100644 --- a/src/components/structures/GroupFilterPanel.js +++ b/src/components/structures/GroupFilterPanel.js @@ -123,12 +123,19 @@ class GroupFilterPanel extends React.Component { mx_GroupFilterPanel_items_selected: itemsSelected, }); + let betaDot; + if (SettingsStore.getBetaInfo("feature_spaces") && !localStorage.getItem("mx_seenSpacesBeta")) { + betaDot =
; + } + let createButton = ( + className="mx_TagTile mx_TagTile_plus"> + { betaDot } + ); if (SettingsStore.getValue("feature_communities_v2_prototypes")) { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 3ab009d7b8..3a2c611cc9 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -36,7 +36,7 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk/src/models/group"; -import {allSettled, sleep} from "../../utils/promise"; +import {sleep} from "../../utils/promise"; import RightPanelStore from "../../stores/RightPanelStore"; import AutoHideScrollbar from "./AutoHideScrollbar"; import {mediaFromMxc} from "../../customisations/Media"; @@ -99,7 +99,7 @@ class CategoryRoomList extends React.Component { onFinished: (success, addrs) => { if (!success) return; const errorList = []; - allSettled(addrs.map((addr) => { + Promise.allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroupSummary(this.props.groupId, addr.address) .catch(() => { errorList.push(addr.address); }); @@ -274,7 +274,7 @@ class RoleUserList extends React.Component { onFinished: (success, addrs) => { if (!success) return; const errorList = []; - allSettled(addrs.map((addr) => { + Promise.allSettled(addrs.map((addr) => { return GroupStore .addUserToGroupSummary(addr.address) .catch(() => { errorList.push(addr.address); }); diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 341ab2df71..51a3b287f0 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -59,7 +59,9 @@ export default class IndicatorScrollbar extends React.Component { _collectScroller(scroller) { if (scroller && !this._scrollElement) { this._scrollElement = scroller; - this._scrollElement.addEventListener("scroll", this.checkOverflow); + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true }); this.checkOverflow(); } } diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 7f9ef7516e..5b6b9c3717 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -43,6 +43,7 @@ import {replaceableComponent} from "../../utils/replaceableComponent"; import {mediaFromMxc} from "../../customisations/Media"; import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; +import UIStore from "../../stores/UIStore"; interface IProps { isMinimized: boolean; @@ -66,6 +67,7 @@ const cssClasses = [ @replaceableComponent("structures.LeftPanel") export default class LeftPanel extends React.Component { + private ref: React.RefObject = createRef(); private listContainerRef: React.RefObject = createRef(); private groupFilterPanelWatcherRef: string; private bgImageWatcherRef: string; @@ -90,10 +92,14 @@ export default class LeftPanel extends React.Component { this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); }); + } - // We watch the middle panel because we don't actually get resized, the middle panel does. - // We listen to the noisy channel to avoid choppy reaction times. - this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); + public componentDidMount() { + UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); + UIStore.instance.on("ListContainer", this.refreshStickyHeaders); + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + this.listContainerRef.current?.addEventListener("scroll", this.onScroll, { passive: true }); } public componentWillUnmount() { @@ -103,7 +109,15 @@ export default class LeftPanel extends React.Component { RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); - this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); + UIStore.instance.stopTrackingElementDimensions("ListContainer"); + UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders); + this.listContainerRef.current?.removeEventListener("scroll", this.onScroll); + } + + public componentDidUpdate(prevProps: IProps, prevState: IState): void { + if (prevState.activeSpace !== this.state.activeSpace) { + this.refreshStickyHeaders(); + } } private updateActiveSpace = (activeSpace: Room) => { @@ -114,6 +128,11 @@ export default class LeftPanel extends React.Component { dis.fire(Action.ViewRoomDirectory); }; + private refreshStickyHeaders = () => { + if (!this.listContainerRef.current) return; // ignore: no headers to sticky + this.handleStickyHeaders(this.listContainerRef.current); + } + private onBreadcrumbsUpdate = () => { const newVal = BreadcrumbsStore.instance.visible; if (newVal !== this.state.showBreadcrumbs) { @@ -156,9 +175,6 @@ export default class LeftPanel extends React.Component { const bottomEdge = list.offsetHeight + list.scrollTop; const sublists = list.querySelectorAll(".mx_RoomSublist:not(.mx_RoomSublist_hidden)"); - const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles - const headerStickyWidth = list.clientWidth - headerRightMargin; - // We track which styles we want on a target before making the changes to avoid // excessive layout updates. const targetStyles = new Map { header.classList.add("mx_RoomSublist_headerContainer_stickyBottom"); } - const offset = window.innerHeight - (list.parentElement.offsetTop + list.parentElement.offsetHeight); + const offset = UIStore.instance.windowHeight - + (list.parentElement.offsetTop + list.parentElement.offsetHeight); const newBottom = `${offset}px`; if (header.style.bottom !== newBottom) { header.style.bottom = newBottom; @@ -247,14 +264,20 @@ export default class LeftPanel extends React.Component { header.classList.add("mx_RoomSublist_headerContainer_sticky"); } - const newWidth = `${headerStickyWidth}px`; - if (header.style.width !== newWidth) { - header.style.width = newWidth; + const listDimensions = UIStore.instance.getElementDimensions("ListContainer"); + if (listDimensions) { + const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles + const headerStickyWidth = listDimensions.width - headerRightMargin; + const newWidth = `${headerStickyWidth}px`; + if (header.style.width !== newWidth) { + header.style.width = newWidth; + } } } else if (!style.stickyTop && !style.stickyBottom) { if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) { header.classList.remove("mx_RoomSublist_headerContainer_sticky"); } + if (header.style.width) { header.style.removeProperty('width'); } @@ -276,16 +299,11 @@ export default class LeftPanel extends React.Component { } } - private onScroll = (ev: React.MouseEvent) => { + private onScroll = (ev: Event) => { const list = ev.target as HTMLDivElement; this.handleStickyHeaders(list); }; - private onResize = () => { - if (!this.listContainerRef.current) return; // ignore: no headers to sticky - this.handleStickyHeaders(this.listContainerRef.current); - }; - private onFocus = (ev: React.FocusEvent) => { this.focusedElement = ev.target; }; @@ -420,8 +438,8 @@ export default class LeftPanel extends React.Component { onFocus={this.onFocus} onBlur={this.onBlur} isMinimized={this.props.isMinimized} - onResize={this.onResize} activeSpace={this.state.activeSpace} + onListCollapse={this.refreshStickyHeaders} />; const containerClasses = classNames({ @@ -435,17 +453,16 @@ export default class LeftPanel extends React.Component { ); return ( -
+
{leftLeftPanel}
); diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx index e88af282ba..16142069c4 100644 --- a/src/components/structures/LeftPanelWidget.tsx +++ b/src/components/structures/LeftPanelWidget.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useContext, useEffect, useMemo} from "react"; +import React, {useContext, useMemo} from "react"; import {Resizable} from "re-resizable"; import classNames from "classnames"; @@ -27,16 +27,13 @@ import WidgetUtils, {IWidgetEvent} from "../../utils/WidgetUtils"; import {useAccountData} from "../../hooks/useAccountData"; import AppTile from "../views/elements/AppTile"; import {useSettingValue} from "../../hooks/useSettings"; - -interface IProps { - onResize(): void; -} +import UIStore from "../../stores/UIStore"; const MIN_HEIGHT = 100; const MAX_HEIGHT = 500; // or 50% of the window height const INITIAL_HEIGHT = 280; -const LeftPanelWidget: React.FC = ({ onResize }) => { +const LeftPanelWidget: React.FC = () => { const cli = useContext(MatrixClientContext); const mWidgetsEvent = useAccountData>(cli, "m.widgets"); @@ -56,7 +53,6 @@ const LeftPanelWidget: React.FC = ({ onResize }) => { const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT); const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true); - useEffect(onResize, [expanded, onResize]); const [onFocus, isActive, ref] = useRovingTabIndex(); const tabIndex = isActive ? 0 : -1; @@ -68,8 +64,7 @@ const LeftPanelWidget: React.FC = ({ onResize }) => { content = { setHeight(height + d.height); }} diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index c4b9696807..3091278e3a 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -27,7 +27,7 @@ import CallMediaHandler from '../../CallMediaHandler'; import { fixupColorFonts } from '../../utils/FontManager'; import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; -import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg'; +import { IMatrixClientCreds } from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; import TagOrderActions from '../../actions/TagOrderActions'; @@ -219,16 +219,6 @@ class LoggedInView extends React.Component { }); }; - // Child components assume that the client peg will not be null, so give them some - // sort of assurance here by only allowing a re-render if the client is truthy. - // - // This is required because `LoggedInView` maintains its own state and if this state - // updates after the client peg has been made null (during logout), then it will - // attempt to re-render and the children will throw errors. - shouldComponentUpdate() { - return Boolean(MatrixClientPeg.get()); - } - canResetTimelineInRoom = (roomId) => { if (!this._roomView.current) { return true; @@ -368,7 +358,7 @@ class LoggedInView extends React.Component { const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); for (const eventId of pinnedEventIds) { - const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); + const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId); const event = timeline.getEvents().find(ev => ev.getId() === eventId); if (event) events.push(event); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index e330dc7d38..16da9321e2 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -86,6 +86,9 @@ import {RoomUpdateCause} from "../../stores/room-list/models"; import defaultDispatcher from "../../dispatcher/dispatcher"; import SecurityCustomisations from "../../customisations/Security"; +import PerformanceMonitor, { PerformanceEntryNames } from "../../performance"; +import UIStore, { UI_EVENTS } from "../../stores/UIStore"; + /** constants for MatrixChat.state.view */ export enum Views { // a special initial state which is only used at startup, while we are @@ -223,13 +226,13 @@ export default class MatrixChat extends React.PureComponent { firstSyncPromise: IDeferred; private screenAfterLogin?: IScreen; - private windowWidth: number; private pageChanging: boolean; private tokenLogin?: boolean; private accountPassword?: string; private accountPasswordTimer?: NodeJS.Timeout; private focusComposer: boolean; private subTitleStatus: string; + private prevWindowWidth: number; private readonly loggedInView: React.RefObject; private readonly dispatcherRef: any; @@ -275,9 +278,8 @@ export default class MatrixChat extends React.PureComponent { } } - this.windowWidth = 10000; - this.handleResize(); - window.addEventListener('resize', this.handleResize); + this.prevWindowWidth = UIStore.instance.windowWidth || 1000; + UIStore.instance.on(UI_EVENTS.Resize, this.handleResize); this.pageChanging = false; @@ -376,7 +378,7 @@ export default class MatrixChat extends React.PureComponent { this.onLoggedIn(); } - const promisesList = [this.firstSyncPromise.promise]; + const promisesList: Promise[] = [this.firstSyncPromise.promise]; if (cryptoEnabled) { // wait for the client to finish downloading cross-signing keys for us so we // know whether or not we have keys set up on this account @@ -434,7 +436,7 @@ export default class MatrixChat extends React.PureComponent { dis.unregister(this.dispatcherRef); this.themeWatcher.stop(); this.fontWatcher.stop(); - window.removeEventListener('resize', this.handleResize); + UIStore.destroy(); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); @@ -484,42 +486,22 @@ export default class MatrixChat extends React.PureComponent { } startPageChangeTimer() { - // Tor doesn't support performance - if (!performance || !performance.mark) return null; - - // This shouldn't happen because UNSAFE_componentWillUpdate and componentDidUpdate - // are used. - if (this.pageChanging) { - console.warn('MatrixChat.startPageChangeTimer: timer already started'); - return; - } - this.pageChanging = true; - performance.mark('element_MatrixChat_page_change_start'); + PerformanceMonitor.instance.start(PerformanceEntryNames.PAGE_CHANGE); } stopPageChangeTimer() { - // Tor doesn't support performance - if (!performance || !performance.mark) return null; + const perfMonitor = PerformanceMonitor.instance; - if (!this.pageChanging) { - console.warn('MatrixChat.stopPageChangeTimer: timer not started'); - return; - } - this.pageChanging = false; - performance.mark('element_MatrixChat_page_change_stop'); - performance.measure( - 'element_MatrixChat_page_change_delta', - 'element_MatrixChat_page_change_start', - 'element_MatrixChat_page_change_stop', - ); - performance.clearMarks('element_MatrixChat_page_change_start'); - performance.clearMarks('element_MatrixChat_page_change_stop'); - const measurement = performance.getEntriesByName('element_MatrixChat_page_change_delta').pop(); + perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE); - // In practice, sometimes the entries list is empty, so we get no measurement - if (!measurement) return null; + const entries = perfMonitor.getEntries({ + name: PerformanceEntryNames.PAGE_CHANGE, + }); + const measurement = entries.pop(); - return measurement.duration; + return measurement + ? measurement.duration + : null; } shouldTrackPageChange(prevState: IState, state: IState) { @@ -683,7 +665,7 @@ export default class MatrixChat extends React.PureComponent { break; } case 'view_create_room': - this.createRoom(payload.public); + this.createRoom(payload.public, payload.defaultName); break; case 'view_create_group': { let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog") @@ -740,6 +722,8 @@ export default class MatrixChat extends React.PureComponent { this.showScreenAfterLogin(); break; case 'toggle_my_groups': + // persist that the user has interacted with this, use it to dismiss the beta dot + localStorage.setItem("mx_seenSpacesBeta", "1"); // We just dispatch the page change rather than have to worry about // what the logic is for each of these branches. if (this.state.page_type === PageTypes.MyGroups) { @@ -906,6 +890,11 @@ export default class MatrixChat extends React.PureComponent { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { + // Not all timeline events are decrypted ahead of time anymore + // Only the critical ones for a typical UI are + // This will start the decryption process for all events when a + // user views a room + room.decryptAllEvents(); const theAlias = Rooms.getDisplayAliasForRoom(room); if (theAlias) { presentedId = theAlias; @@ -1022,7 +1011,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private async createRoom(defaultPublic = false) { + private async createRoom(defaultPublic = false, defaultName?: string) { const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId(); if (communityId) { // double check the user will have permission to associate this room with the community @@ -1036,7 +1025,10 @@ export default class MatrixChat extends React.PureComponent { } const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); - const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic }); + const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { + defaultPublic, + defaultName, + }); const [shouldCreate, opts] = await modal.finished; if (shouldCreate) { @@ -1625,11 +1617,13 @@ export default class MatrixChat extends React.PureComponent { action: 'start_registration', params: params, }); + PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER); } else if (screen === 'login') { dis.dispatch({ action: 'start_login', params: params, }); + PerformanceMonitor.instance.start(PerformanceEntryNames.LOGIN); } else if (screen === 'forgot_password') { dis.dispatch({ action: 'start_password_recovery', @@ -1684,6 +1678,10 @@ export default class MatrixChat extends React.PureComponent { const type = screen === "start_sso" ? "sso" : "cas"; PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin()); } else if (screen === 'groups') { + if (SettingsStore.getValue("feature_spaces")) { + dis.dispatch({ action: "view_home_page" }); + return; + } dis.dispatch({ action: 'view_my_groups', }); @@ -1767,6 +1765,11 @@ export default class MatrixChat extends React.PureComponent { subAction: params.action, }); } else if (screen.indexOf('group/') === 0) { + if (SettingsStore.getValue("feature_spaces")) { + dis.dispatch({ action: "view_home_page" }); + return; + } + const groupId = screen.substring(6); // TODO: Check valid group ID @@ -1817,18 +1820,19 @@ export default class MatrixChat extends React.PureComponent { } handleResize = () => { - const hideLhsThreshold = 1000; - const showLhsThreshold = 1000; + const LHS_THRESHOLD = 1000; + const width = UIStore.instance.windowWidth; - if (this.windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { - dis.dispatch({ action: 'hide_left_panel' }); - } - if (this.windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) { + if (this.prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD) { dis.dispatch({ action: 'show_left_panel' }); } + if (this.prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD) { + dis.dispatch({ action: 'hide_left_panel' }); + } + + this.prevWindowWidth = width; this.state.resizeNotifier.notifyWindowResized(); - this.windowWidth = window.innerWidth; }; private dispatchTimelineResize() { @@ -1949,6 +1953,8 @@ export default class MatrixChat extends React.PureComponent { // Create and start the client await Lifecycle.setLoggedIn(credentials); await this.postLoginSetup(); + PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN); + PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER); }; // complete security / e2e setup has finished @@ -2085,6 +2091,7 @@ export default class MatrixChat extends React.PureComponent { onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined} onServerConfigChange={this.onServerConfigChange} fragmentAfterLogin={fragmentAfterLogin} + defaultUsername={this.props.startingFragmentQueryParams.defaultUsername} {...this.getServerProperties()} /> ); diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index c93f07fa0f..6709fef814 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -34,6 +34,7 @@ import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResiz import DMRoomMap from "../../utils/DMRoomMap"; import NewRoomIntro from "../views/rooms/NewRoomIntro"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import defaultDispatcher from '../../dispatcher/dispatcher'; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -120,6 +121,9 @@ export default class MessagePanel extends React.Component { // callback which is called when the panel is scrolled. onScroll: PropTypes.func, + // callback which is called when the user interacts with the room timeline + onUserScroll: PropTypes.func, + // callback which is called when more content is needed. onFillRequest: PropTypes.func, @@ -471,6 +475,10 @@ export default class MessagePanel extends React.Component { return {nextEvent, nextTile}; } + get _roomHasPendingEdit() { + return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`); + } + _getEventTiles() { this.eventNodes = {}; @@ -544,11 +552,13 @@ export default class MessagePanel extends React.Component { } if (!grouper) { const wantTile = this._shouldShowEvent(mxEv); + const isGrouped = false; if (wantTile) { // make sure we unpack the array returned by _getTilesForEvent, // otherwise react will auto-generate keys and we will end up // replacing all of the DOM elements every time we paginate. - ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextTile)); + ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, isGrouped, + nextEvent, nextTile)); prevEvent = mxEv; } @@ -557,6 +567,13 @@ export default class MessagePanel extends React.Component { } } + if (!this.props.editState && this._roomHasPendingEdit) { + defaultDispatcher.dispatch({ + action: "edit_event", + event: this.props.room.findEventById(this._roomHasPendingEdit), + }); + } + if (grouper) { ret.push(...grouper.getTiles()); } @@ -564,7 +581,7 @@ export default class MessagePanel extends React.Component { return ret; } - _getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) { + _getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) { const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary'); const EventTile = sdk.getComponent('rooms.EventTile'); const DateSeparator = sdk.getComponent('messages.DateSeparator'); @@ -572,7 +589,6 @@ export default class MessagePanel extends React.Component { const isEditing = this.props.editState && this.props.editState.getEvent().getId() === mxEv.getId(); - // local echoes have a fake date, which could even be yesterday. Treat them // as 'today' for the date separators. let ts1 = mxEv.getTs(); @@ -584,7 +600,7 @@ export default class MessagePanel extends React.Component { // do we need a date separator since the last event? const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate); - if (wantsDateSeparator) { + if (wantsDateSeparator && !isGrouped) { const dateSeparator =
  • ; ret.push(dateSeparator); } @@ -632,39 +648,37 @@ export default class MessagePanel extends React.Component { // use txnId as key if available so that we don't remount during sending ret.push( -
  • - - - -
  • , + + + , ); return ret; @@ -766,7 +780,7 @@ export default class MessagePanel extends React.Component { } _collectEventNode = (eventId, node) => { - this.eventNodes[eventId] = node; + this.eventNodes[eventId] = node?.ref?.current; } // once dynamic content in the events load, make the scrollPanel check the @@ -872,6 +886,7 @@ export default class MessagePanel extends React.Component { ref={this._scrollPanel} className={className} onScroll={this.props.onScroll} + onUserScroll={this.props.onUserScroll} onResize={this.onResize} onFillRequest={this.props.onFillRequest} onUnfillRequest={this.props.onUnfillRequest} @@ -968,9 +983,9 @@ class CreationGrouper { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); - const panel = this.panel; const ret = []; + const isGrouped = true; const createEvent = this.createEvent; const lastShownEvent = this.lastShownEvent; @@ -984,12 +999,12 @@ class CreationGrouper { // If this m.room.create event should be shown (room upgrade) then show it before the summary if (panel._shouldShowEvent(createEvent)) { // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered - ret.push(...panel._getTilesForEvent(createEvent, createEvent, false)); + ret.push(...panel._getTilesForEvent(createEvent, createEvent)); } for (const ejected of this.ejectedEvents) { ret.push(...panel._getTilesForEvent( - createEvent, ejected, createEvent === lastShownEvent, + createEvent, ejected, createEvent === lastShownEvent, isGrouped, )); } @@ -998,7 +1013,7 @@ class CreationGrouper { // of EventListSummary, render each member event as if the previous // one was itself. This way, the timestamp of the previous event === the // timestamp of the current event, and no DateSeparator is inserted. - return panel._getTilesForEvent(e, e, e === lastShownEvent); + return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped); }).reduce((a, b) => a.concat(b), []); // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one const ev = this.events[this.events.length - 1]; @@ -1083,7 +1098,7 @@ class RedactionGrouper { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); - + const isGrouped = true; const panel = this.panel; const ret = []; const lastShownEvent = this.lastShownEvent; @@ -1103,7 +1118,8 @@ class RedactionGrouper { let eventTiles = this.events.map((e, i) => { senders.add(e.sender); const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1]; - return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, this.nextEvent, this.nextEventTile); + return panel._getTilesForEvent( + prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { @@ -1182,7 +1198,7 @@ class MemberGrouper { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); - + const isGrouped = true; const panel = this.panel; const lastShownEvent = this.lastShownEvent; const ret = []; @@ -1215,7 +1231,7 @@ class MemberGrouper { // of MemberEventListSummary, render each member event as if the previous // one was itself. This way, the timestamp of the previous event === the // timestamp of the current event, and no DateSeparator is inserted. - return panel._getTilesForEvent(e, e, e === lastShownEvent); + return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 2ab11dad25..1fab6c4348 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -25,6 +25,7 @@ import AccessibleButton from '../views/elements/AccessibleButton'; import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import BetaCard from "../views/beta/BetaCard"; @replaceableComponent("structures.MyGroups") export default class MyGroups extends React.Component { @@ -139,6 +140,7 @@ export default class MyGroups extends React.Component {
    */} +
    { contentHeader } { content } diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.tsx similarity index 70% rename from src/components/structures/NotificationPanel.js rename to src/components/structures/NotificationPanel.tsx index 41aafc8b13..b4f13f6b37 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.tsx @@ -1,7 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,29 +14,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from "prop-types"; +import React from "react"; import { _t } from '../../languageHandler'; -import {MatrixClientPeg} from "../../MatrixClientPeg"; -import * as sdk from "../../index"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; import BaseCard from "../views/right_panel/BaseCard"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { replaceableComponent } from "../../utils/replaceableComponent"; +import TimelinePanel from "./TimelinePanel"; +import Spinner from "../views/elements/Spinner"; + +interface IProps { + onClose(): void; +} /* * Component which shows the global notification list using a TimelinePanel */ @replaceableComponent("structures.NotificationPanel") -class NotificationPanel extends React.Component { - static propTypes = { - onClose: PropTypes.func.isRequired, - }; - +export default class NotificationPanel extends React.PureComponent { render() { - // wrap a TimelinePanel with the jump-to-event bits turned off. - const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); - const Loader = sdk.getComponent("elements.Spinner"); - const emptyState = (

    {_t('You’re all caught up')}

    {_t('You have no visible notifications.')}

    @@ -47,6 +41,7 @@ class NotificationPanel extends React.Component { let content; const timelineSet = MatrixClientPeg.get().getNotifTimelineSet(); if (timelineSet) { + // wrap a TimelinePanel with the jump-to-event bits turned off. content = ( ; + content = ; } return @@ -67,5 +62,3 @@ class NotificationPanel extends React.Component { ; } } - -export default NotificationPanel; diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.tsx similarity index 77% rename from src/components/structures/RightPanel.js rename to src/components/structures/RightPanel.tsx index d8c763eabd..294865fe08 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.tsx @@ -1,6 +1,6 @@ /* Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2015 - 2020 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,70 +16,92 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {Room} from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { User } from "matrix-js-sdk/src/models/user"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; -import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; import { - RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS, RIGHT_PANEL_SPACE_PHASES, + RightPanelPhases, } from "../../stores/RightPanelStorePhases"; import RightPanelStore from "../../stores/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; -import {Action} from "../../dispatcher/actions"; +import { Action } from "../../dispatcher/actions"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import SettingsStore from "../../settings/SettingsStore"; +import { ActionPayload } from "../../dispatcher/payloads"; +import MemberList from "../views/rooms/MemberList"; +import GroupMemberList from "../views/groups/GroupMemberList"; +import GroupRoomList from "../views/groups/GroupRoomList"; +import GroupRoomInfo from "../views/groups/GroupRoomInfo"; +import UserInfo from "../views/right_panel/UserInfo"; +import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo"; +import FilePanel from "./FilePanel"; +import NotificationPanel from "./NotificationPanel"; +import ResizeNotifier from "../../utils/ResizeNotifier"; +import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; + +interface IProps { + room?: Room; // if showing panels for a given room, this is set + groupId?: string; // if showing panels for a given group, this is set + user?: User; // used if we know the user ahead of opening the panel + resizeNotifier: ResizeNotifier; +} + +interface IState { + phase: RightPanelPhases; + isUserPrivilegedInGroup?: boolean; + member?: RoomMember; + verificationRequest?: VerificationRequest; + verificationRequestPromise?: Promise; + space?: Room; + widgetId?: string; + groupRoomId?: string; + groupId?: string; + event: MatrixEvent; +} @replaceableComponent("structures.RightPanel") -export default class RightPanel extends React.Component { - static get propTypes() { - return { - room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set - groupId: PropTypes.string, // if showing panels for a given group, this is set - user: PropTypes.object, // used if we know the user ahead of opening the panel - }; - } - +export default class RightPanel extends React.Component { static contextType = MatrixClientContext; + private readonly delayedUpdate: RateLimitedFunc; + private dispatcherRef: string; + constructor(props, context) { super(props, context); this.state = { ...RightPanelStore.getSharedInstance().roomPanelPhaseParams, - phase: this._getPhaseFromProps(), + phase: this.getPhaseFromProps(), isUserPrivilegedInGroup: null, - member: this._getUserForPanel(), + member: this.getUserForPanel(), }; - this.onAction = this.onAction.bind(this); - this.onRoomStateMember = this.onRoomStateMember.bind(this); - this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this); - this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this); - this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this); - this._delayedUpdate = new RateLimitedFunc(() => { + this.delayedUpdate = new RateLimitedFunc(() => { this.forceUpdate(); }, 500); } - // Helper function to split out the logic for _getPhaseFromProps() and the constructor + // Helper function to split out the logic for getPhaseFromProps() and the constructor // as both are called at the same time in the constructor. - _getUserForPanel() { + private getUserForPanel() { if (this.state && this.state.member) return this.state.member; const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams; return this.props.user || lastParams['member']; } // gets the current phase from the props and also maybe the store - _getPhaseFromProps() { + private getPhaseFromProps() { const rps = RightPanelStore.getSharedInstance(); - const userForPanel = this._getUserForPanel(); + const userForPanel = this.getUserForPanel(); if (this.props.groupId) { if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) { dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList}); @@ -118,7 +140,7 @@ export default class RightPanel extends React.Component { this.dispatcherRef = dis.register(this.onAction); const cli = this.context; cli.on("RoomState.members", this.onRoomStateMember); - this._initGroupStore(this.props.groupId); + this.initGroupStore(this.props.groupId); } componentWillUnmount() { @@ -126,61 +148,47 @@ export default class RightPanel extends React.Component { if (this.context) { this.context.removeListener("RoomState.members", this.onRoomStateMember); } - this._unregisterGroupStore(this.props.groupId); + this.unregisterGroupStore(); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase if (newProps.groupId !== this.props.groupId) { - this._unregisterGroupStore(this.props.groupId); - this._initGroupStore(newProps.groupId); + this.unregisterGroupStore(); + this.initGroupStore(newProps.groupId); } } - _initGroupStore(groupId) { + private initGroupStore(groupId: string) { if (!groupId) return; GroupStore.registerListener(groupId, this.onGroupStoreUpdated); } - _unregisterGroupStore() { + private unregisterGroupStore() { GroupStore.unregisterListener(this.onGroupStoreUpdated); } - onGroupStoreUpdated() { + private onGroupStoreUpdated = () => { this.setState({ isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId), }); - } + }; - onInviteToGroupButtonClick() { - showGroupInviteDialog(this.props.groupId).then(() => { - this.setState({ - phase: RightPanelPhases.GroupMemberList, - }); - }); - } - - onAddRoomToGroupButtonClick() { - showGroupAddRoomDialog(this.props.groupId).then(() => { - this.forceUpdate(); - }); - } - - onRoomStateMember(ev, state, member) { + private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => { if (!this.props.room || member.roomId !== this.props.room.roomId) { return; } // redraw the badge on the membership list if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) { - this._delayedUpdate(); + this.delayedUpdate(); } else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId && member.userId === this.state.member.userId) { // refresh the member info (e.g. new power level) - this._delayedUpdate(); + this.delayedUpdate(); } - } + }; - onAction(payload) { + private onAction = (payload: ActionPayload) => { if (payload.action === Action.AfterRightPanelPhaseChange) { this.setState({ phase: payload.phase, @@ -194,9 +202,9 @@ export default class RightPanel extends React.Component { space: payload.space, }); } - } + }; - onClose = () => { + private onClose = () => { // XXX: There are three different ways of 'closing' this panel depending on what state // things are in... this knows far more than it should do about the state of the rest // of the app and is generally a bit silly. @@ -224,16 +232,6 @@ export default class RightPanel extends React.Component { }; render() { - const MemberList = sdk.getComponent('rooms.MemberList'); - const UserInfo = sdk.getComponent('right_panel.UserInfo'); - const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo'); - const NotificationPanel = sdk.getComponent('structures.NotificationPanel'); - const FilePanel = sdk.getComponent('structures.FilePanel'); - - const GroupMemberList = sdk.getComponent('groups.GroupMemberList'); - const GroupRoomList = sdk.getComponent('groups.GroupRoomList'); - const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo'); - let panel =
    ; const roomId = this.props.room ? this.props.room.roomId : undefined; @@ -285,6 +283,7 @@ export default class RightPanel extends React.Component { user={this.state.member} groupId={this.props.groupId} key={this.state.member.userId} + phase={this.state.phase} onClose={this.onClose} />; break; @@ -299,6 +298,12 @@ export default class RightPanel extends React.Component { panel = ; break; + case RightPanelPhases.PinnedMessages: + if (SettingsStore.getValue("feature_pinning")) { + panel = ; + } + break; + case RightPanelPhases.FilePanel: panel = ; break; diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.tsx similarity index 71% rename from src/components/structures/RoomDirectory.js rename to src/components/structures/RoomDirectory.tsx index 3613261da6..1e0605f263 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.tsx @@ -1,7 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2019, 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,39 +15,90 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import {MatrixClientPeg} from "../../MatrixClientPeg"; -import * as sdk from "../../index"; +import React from "react"; + +import { MatrixClientPeg } from "../../MatrixClientPeg"; import dis from "../../dispatcher/dispatcher"; import Modal from "../../Modal"; import { linkifyAndSanitizeHtml } from '../../HtmlUtils'; -import PropTypes from 'prop-types'; import { _t } from '../../languageHandler'; import SdkConfig from '../../SdkConfig'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; -import {ALL_ROOMS} from "../views/directory/NetworkDropdown"; +import {ALL_ROOMS, IFieldType, IInstance, IProtocol, Protocols} from "../views/directory/NetworkDropdown"; import SettingsStore from "../../settings/SettingsStore"; import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore"; import GroupStore from "../../stores/GroupStore"; import FlairStore from "../../stores/FlairStore"; import CountlyAnalytics from "../../CountlyAnalytics"; -import {replaceableComponent} from "../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../customisations/Media"; +import { replaceableComponent } from "../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../customisations/Media"; +import { IDialogProps } from "../views/dialogs/IDialogProps"; +import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; +import BaseAvatar from "../views/avatars/BaseAvatar"; +import ErrorDialog from "../views/dialogs/ErrorDialog"; +import QuestionDialog from "../views/dialogs/QuestionDialog"; +import BaseDialog from "../views/dialogs/BaseDialog"; +import DirectorySearchBox from "../views/elements/DirectorySearchBox"; +import NetworkDropdown from "../views/directory/NetworkDropdown"; +import ScrollPanel from "./ScrollPanel"; +import Spinner from "../views/elements/Spinner"; +import { ActionPayload } from "../../dispatcher/payloads"; + const MAX_NAME_LENGTH = 80; const MAX_TOPIC_LENGTH = 800; -function track(action) { +function track(action: string) { Analytics.trackEvent('RoomDirectory', action); } +interface IProps extends IDialogProps { + initialText?: string; +} + +interface IState { + publicRooms: IRoom[]; + loading: boolean; + protocolsLoading: boolean; + error?: string; + instanceId: string | symbol; + roomServer: string; + filterString: string; + selectedCommunityId?: string; + communityName?: string; +} + +/* eslint-disable camelcase */ +interface IRoom { + room_id: string; + name?: string; + avatar_url?: string; + topic?: string; + canonical_alias?: string; + aliases?: string[]; + world_readable: boolean; + guest_can_join: boolean; + num_joined_members: number; +} + +interface IPublicRoomsRequest { + limit?: number; + since?: string; + server?: string; + filter?: object; + include_all_networks?: boolean; + third_party_instance_id?: string; +} +/* eslint-enable camelcase */ + @replaceableComponent("structures.RoomDirectory") -export default class RoomDirectory extends React.Component { - static propTypes = { - initialText: PropTypes.string, - onFinished: PropTypes.func.isRequired, - }; +export default class RoomDirectory extends React.Component { + private readonly startTime: number; + private unmounted = false + private nextBatch: string = null; + private filterTimeout: NodeJS.Timeout; + private protocols: Protocols; constructor(props) { super(props); @@ -56,41 +106,21 @@ export default class RoomDirectory extends React.Component { CountlyAnalytics.instance.trackRoomDirectoryBegin(); this.startTime = CountlyAnalytics.getTimestamp(); - const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0]; - this.state = { - publicRooms: [], - loading: true, - protocolsLoading: true, - error: null, - instanceId: undefined, - roomServer: MatrixClientPeg.getHomeserverName(), - filterString: this.props.initialText || "", - selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes") - ? selectedCommunityId - : null, - communityName: null, - }; + const selectedCommunityId = SettingsStore.getValue("feature_communities_v2_prototypes") + ? GroupFilterOrderStore.getSelectedTags()[0] + : null; - this._unmounted = false; - this.nextBatch = null; - this.filterTimeout = null; - this.scrollPanel = null; - this.protocols = null; - - this.state.protocolsLoading = true; + let protocolsLoading = true; if (!MatrixClientPeg.get()) { // We may not have a client yet when invoked from welcome page - this.state.protocolsLoading = false; - return; - } - - if (!this.state.selectedCommunityId) { + protocolsLoading = false; + } else if (!selectedCommunityId) { MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { this.protocols = response; - this.setState({protocolsLoading: false}); + this.setState({ protocolsLoading: false }); }, (err) => { console.warn(`error loading third party protocols: ${err}`); - this.setState({protocolsLoading: false}); + this.setState({ protocolsLoading: false }); if (MatrixClientPeg.get().isGuest()) { // Guests currently aren't allowed to use this API, so // ignore this as otherwise this error is literally the @@ -103,19 +133,31 @@ export default class RoomDirectory extends React.Component { error: _t( '%(brand)s failed to get the protocol list from the homeserver. ' + 'The homeserver may be too old to support third party networks.', - {brand}, + { brand }, ), }); }); } else { // We don't use the protocols in the communities v2 prototype experience - this.state.protocolsLoading = false; + protocolsLoading = false; // Grab the profile info async FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => { - this.setState({communityName: profile.name}); + this.setState({ communityName: profile.name }); }); } + + this.state = { + publicRooms: [], + loading: true, + error: null, + instanceId: undefined, + roomServer: MatrixClientPeg.getHomeserverName(), + filterString: this.props.initialText || "", + selectedCommunityId, + communityName: null, + protocolsLoading, + }; } componentDidMount() { @@ -126,10 +168,10 @@ export default class RoomDirectory extends React.Component { if (this.filterTimeout) { clearTimeout(this.filterTimeout); } - this._unmounted = true; + this.unmounted = true; } - refreshRoomList = () => { + private refreshRoomList = () => { if (this.state.selectedCommunityId) { this.setState({ publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => { @@ -165,7 +207,7 @@ export default class RoomDirectory extends React.Component { this.getMoreRooms(); }; - getMoreRooms() { + private getMoreRooms() { if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms if (!MatrixClientPeg.get()) return Promise.resolve(); @@ -173,34 +215,34 @@ export default class RoomDirectory extends React.Component { loading: true, }); - const my_filter_string = this.state.filterString; - const my_server = this.state.roomServer; + const filterString = this.state.filterString; + const roomServer = this.state.roomServer; // remember the next batch token when we sent the request // too. If it's changed, appending to the list will corrupt it. - const my_next_batch = this.nextBatch; - const opts = {limit: 20}; - if (my_server != MatrixClientPeg.getHomeserverName()) { - opts.server = my_server; + const nextBatch = this.nextBatch; + const opts: IPublicRoomsRequest = { limit: 20 }; + if (roomServer != MatrixClientPeg.getHomeserverName()) { + opts.server = roomServer; } if (this.state.instanceId === ALL_ROOMS) { opts.include_all_networks = true; } else if (this.state.instanceId) { - opts.third_party_instance_id = this.state.instanceId; + opts.third_party_instance_id = this.state.instanceId as string; } if (this.nextBatch) opts.since = this.nextBatch; - if (my_filter_string) opts.filter = { generic_search_term: my_filter_string }; + if (filterString) opts.filter = { generic_search_term: filterString }; return MatrixClientPeg.get().publicRooms(opts).then((data) => { if ( - my_filter_string != this.state.filterString || - my_server != this.state.roomServer || - my_next_batch != this.nextBatch) { + filterString != this.state.filterString || + roomServer != this.state.roomServer || + nextBatch != this.nextBatch) { // if the filter or server has changed since this request was sent, // throw away the result (don't even clear the busy flag // since we must still have a request in flight) return; } - if (this._unmounted) { + if (this.unmounted) { // if we've been unmounted, we don't care either. return; } @@ -211,23 +253,23 @@ export default class RoomDirectory extends React.Component { } this.nextBatch = data.next_batch; - this.setState((s) => { - s.publicRooms.push(...(data.chunk || [])); - s.loading = false; - return s; - }); + this.setState((s) => ({ + ...s, + publicRooms: [...s.publicRooms, ...(data.chunk || [])], + loading: false, + })); return Boolean(data.next_batch); }, (err) => { if ( - my_filter_string != this.state.filterString || - my_server != this.state.roomServer || - my_next_batch != this.nextBatch) { + filterString != this.state.filterString || + roomServer != this.state.roomServer || + nextBatch != this.nextBatch) { // as above: we don't care about errors for old // requests either return; } - if (this._unmounted) { + if (this.unmounted) { // if we've been unmounted, we don't care either. return; } @@ -252,13 +294,10 @@ export default class RoomDirectory extends React.Component { * HS admins to do this through the RoomSettings interface, but * this needs SPEC-417. */ - removeFromDirectory(room) { - const alias = get_display_alias_for_room(room); + private removeFromDirectory(room: IRoom) { + const alias = getDisplayAliasForRoom(room); const name = room.name || alias || _t('Unnamed room'); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - let desc; if (alias) { desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name}); @@ -269,11 +308,10 @@ export default class RoomDirectory extends React.Component { Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, { title: _t('Remove from Directory'), description: desc, - onFinished: (should_delete) => { - if (!should_delete) return; + onFinished: (shouldDelete: boolean) => { + if (!shouldDelete) return; - const Loader = sdk.getComponent("elements.Spinner"); - const modal = Modal.createDialog(Loader); + const modal = Modal.createDialog(Spinner); let step = _t('remove %(name)s from the directory.', {name: name}); MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => { @@ -289,14 +327,16 @@ export default class RoomDirectory extends React.Component { console.error("Failed to " + step + ": " + err); Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, { title: _t('Error'), - description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')), + description: (err && err.message) + ? err.message + : _t('The server may be unavailable or overloaded'), }); }); }, }); } - onRoomClicked = (room, ev) => { + private onRoomClicked = (room: IRoom, ev: ButtonEvent) => { if (ev.shiftKey && !this.state.selectedCommunityId) { ev.preventDefault(); this.removeFromDirectory(room); @@ -305,7 +345,7 @@ export default class RoomDirectory extends React.Component { } }; - onOptionChange = (server, instanceId) => { + private onOptionChange = (server: string, instanceId?: string | symbol) => { // clear next batch so we don't try to load more rooms this.nextBatch = null; this.setState({ @@ -325,13 +365,13 @@ export default class RoomDirectory extends React.Component { // Easiest to just blow away the state & re-fetch. }; - onFillRequest = (backwards) => { + private onFillRequest = (backwards: boolean) => { if (backwards || !this.nextBatch) return Promise.resolve(false); return this.getMoreRooms(); }; - onFilterChange = (alias) => { + private onFilterChange = (alias: string) => { this.setState({ filterString: alias || null, }); @@ -349,7 +389,7 @@ export default class RoomDirectory extends React.Component { }, 700); }; - onFilterClear = () => { + private onFilterClear = () => { // update immediately this.setState({ filterString: null, @@ -360,7 +400,7 @@ export default class RoomDirectory extends React.Component { } }; - onJoinFromSearchClick = (alias) => { + private onJoinFromSearchClick = (alias: string) => { // If we don't have a particular instance id selected, just show that rooms alias if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) { // If the user specified an alias without a domain, add on whichever server is selected @@ -373,9 +413,10 @@ export default class RoomDirectory extends React.Component { // This is a 3rd party protocol. Let's see if we can join it const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId); const instance = instanceForInstanceId(this.protocols, this.state.instanceId); - const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null; + const fields = protocolName + ? this.getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) + : null; if (!fields) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const brand = SdkConfig.get().brand; Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, { title: _t('Unable to join network'), @@ -387,14 +428,12 @@ export default class RoomDirectory extends React.Component { if (resp.length > 0 && resp[0].alias) { this.showRoomAlias(resp[0].alias, true); } else { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Room not found', '', ErrorDialog, { title: _t('Room not found'), description: _t('Couldn\'t find a matching Matrix room'), }); } }, (e) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, { title: _t('Fetching third party location failed'), description: _t('Unable to look up room ID from server'), @@ -403,36 +442,37 @@ export default class RoomDirectory extends React.Component { } }; - onPreviewClick = (ev, room) => { + private onPreviewClick = (ev: ButtonEvent, room: IRoom) => { this.showRoom(room, null, false, true); ev.stopPropagation(); }; - onViewClick = (ev, room) => { + private onViewClick = (ev: ButtonEvent, room: IRoom) => { this.showRoom(room); ev.stopPropagation(); }; - onJoinClick = (ev, room) => { + private onJoinClick = (ev: ButtonEvent, room: IRoom) => { this.showRoom(room, null, true); ev.stopPropagation(); }; - onCreateRoomClick = room => { + private onCreateRoomClick = () => { this.onFinished(); dis.dispatch({ action: 'view_create_room', public: true, + defaultName: this.state.filterString.trim(), }); }; - showRoomAlias(alias, autoJoin=false) { + private showRoomAlias(alias: string, autoJoin = false) { this.showRoom(null, alias, autoJoin); } - showRoom(room, room_alias, autoJoin = false, shouldPeek = false) { + private showRoom(room: IRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) { this.onFinished(); - const payload = { + const payload: ActionPayload = { action: 'view_room', auto_join: autoJoin, should_peek: shouldPeek, @@ -449,15 +489,15 @@ export default class RoomDirectory extends React.Component { } } - if (!room_alias) { - room_alias = get_display_alias_for_room(room); + if (!roomAlias) { + roomAlias = getDisplayAliasForRoom(room); } payload.oob_data = { avatarUrl: room.avatar_url, // XXX: This logic is duplicated from the JS SDK which // would normally decide what the name is. - name: room.name || room_alias || _t('Unnamed room'), + name: room.name || roomAlias || _t('Unnamed room'), }; if (this.state.roomServer) { @@ -471,21 +511,19 @@ export default class RoomDirectory extends React.Component { // which servers to start querying. However, there's no other way to join rooms in // this list without aliases at present, so if roomAlias isn't set here we have no // choice but to supply the ID. - if (room_alias) { - payload.room_alias = room_alias; + if (roomAlias) { + payload.room_alias = roomAlias; } else { payload.room_id = room.room_id; } dis.dispatch(payload); } - createRoomCells(room) { + private createRoomCells(room: IRoom) { const client = MatrixClientPeg.get(); const clientRoom = client.getRoom(room.room_id); const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join"; const isGuest = client.isGuest(); - const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let previewButton; let joinOrViewButton; @@ -495,20 +533,26 @@ export default class RoomDirectory extends React.Component { // it is readable, the preview appears as normal. if (!hasJoinedRoom && (room.world_readable || isGuest)) { previewButton = ( - this.onPreviewClick(ev, room)}>{_t("Preview")} + this.onPreviewClick(ev, room)}> + { _t("Preview") } + ); } if (hasJoinedRoom) { joinOrViewButton = ( - this.onViewClick(ev, room)}>{_t("View")} + this.onViewClick(ev, room)}> + { _t("View") } + ); } else if (!isGuest) { joinOrViewButton = ( - this.onJoinClick(ev, room)}>{_t("Join")} + this.onJoinClick(ev, room)}> + { _t("Join") } + ); } - let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room'); + let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room'); if (name.length > MAX_NAME_LENGTH) { name = `${name.substring(0, MAX_NAME_LENGTH)}...`; } @@ -531,9 +575,13 @@ export default class RoomDirectory extends React.Component { onMouseDown={(ev) => {ev.preventDefault();}} className="mx_RoomDirectory_roomAvatar" > -
    ,
    { ev.stopPropagation(); } } dangerouslySetInnerHTML={{ __html: topic }} /> -
    { get_display_alias_for_room(room) }
    +
    { getDisplayAliasForRoom(room) }
    ,
    this.onRoomClicked(room, ev)} @@ -576,20 +624,16 @@ export default class RoomDirectory extends React.Component { ]; } - collectScrollPanel = (element) => { - this.scrollPanel = element; - }; - - _stringLooksLikeId(s, field_type) { + private stringLooksLikeId(s: string, fieldType: IFieldType) { let pat = /^#[^\s]+:[^\s]/; - if (field_type && field_type.regexp) { - pat = new RegExp(field_type.regexp); + if (fieldType && fieldType.regexp) { + pat = new RegExp(fieldType.regexp); } return pat.test(s); } - _getFieldsForThirdPartyLocation(userInput, protocol, instance) { + private getFieldsForThirdPartyLocation(userInput: string, protocol: IProtocol, instance: IInstance) { // make an object with the fields specified by that protocol. We // require that the values of all but the last field come from the // instance. The last is the user input. @@ -605,71 +649,73 @@ export default class RoomDirectory extends React.Component { return fields; } - /** - * called by the parent component when PageUp/Down/etc is pressed. - * - * We pass it down to the scroll panel. - */ - handleScrollKey = ev => { - if (this.scrollPanel) { - this.scrollPanel.handleScrollKey(ev); - } - }; - - onFinished = () => { + private onFinished = () => { CountlyAnalytics.instance.trackRoomDirectory(this.startTime); - this.props.onFinished(); + this.props.onFinished(false); }; render() { - const Loader = sdk.getComponent("elements.Spinner"); - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let content; if (this.state.error) { content = this.state.error; } else if (this.state.protocolsLoading) { - content = ; + content = ; } else { const cells = (this.state.publicRooms || []) - .reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],); + .reduce((cells, room) => cells.concat(this.createRoomCells(room)), []); // we still show the scrollpanel, at least for now, because // otherwise we don't fetch more because we don't get a fill // request from the scrollpanel because there isn't one let spinner; if (this.state.loading) { - spinner = ; + spinner = ; } - let scrollpanel_content; + const createNewButton = <> +
    + + { _t("Create new room") } + + ; + + let scrollPanelContent; + let footer; if (cells.length === 0 && !this.state.loading) { - scrollpanel_content = { _t('No rooms to show') }; + footer = <> +
    { _t('No results for "%(query)s"', { query: this.state.filterString.trim() }) }
    +

    + { _t("Try different words or check for typos. " + + "Some results may not be visible as they're private and you need an invite to join them.") } +

    + { createNewButton } + ; } else { - scrollpanel_content =
    + scrollPanelContent =
    { cells }
    ; + if (!this.state.loading && !this.nextBatch) { + footer = createNewButton; + } } - const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); - content = - { scrollpanel_content } + { scrollPanelContent } { spinner } + { footer &&
    + { footer } +
    }
    ; } let listHeader; if (!this.state.protocolsLoading) { - const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown'); - const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox'); - const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId); - let instance_expected_field_type; + let instanceExpectedFieldType; if ( protocolName && this.protocols && @@ -677,21 +723,27 @@ export default class RoomDirectory extends React.Component { this.protocols[protocolName].location_fields.length > 0 && this.protocols[protocolName].field_types ) { - const last_field = this.protocols[protocolName].location_fields.slice(-1)[0]; - instance_expected_field_type = this.protocols[protocolName].field_types[last_field]; + const lastField = this.protocols[protocolName].location_fields.slice(-1)[0]; + instanceExpectedFieldType = this.protocols[protocolName].field_types[lastField]; } let placeholder = _t('Find a room…'); if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) { - placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer}); - } else if (instance_expected_field_type) { - placeholder = instance_expected_field_type.placeholder; + placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", { + exampleRoom: "#example:" + this.state.roomServer, + }); + } else if (instanceExpectedFieldType) { + placeholder = instanceExpectedFieldType.placeholder; } - let showJoinButton = this._stringLooksLikeId(this.state.filterString, instance_expected_field_type); + let showJoinButton = this.stringLooksLikeId(this.state.filterString, instanceExpectedFieldType); if (protocolName) { const instance = instanceForInstanceId(this.protocols, this.state.instanceId); - if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.protocols[protocolName], instance) === null) { + if (this.getFieldsForThirdPartyLocation( + this.state.filterString, + this.protocols[protocolName], + instance, + ) === null) { showJoinButton = false; } } @@ -723,12 +775,11 @@ export default class RoomDirectory extends React.Component { } const explanation = _t("If you can't find the room you're looking for, ask for an invite or Create a new room.", null, - {a: sub => { - return ({sub}); - }}, + {a: sub => ( + + { sub } + + )}, ); const title = this.state.selectedCommunityId @@ -756,6 +807,6 @@ export default class RoomDirectory extends React.Component { // Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom // but works with the objects we get from the public room list -function get_display_alias_for_room(room) { - return room.canonical_alias || (room.aliases ? room.aliases[0] : ""); +function getDisplayAliasForRoom(room: IRoom) { + return room.canonical_alias || room.aliases?.[0] || ""; } diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 34682877e0..bda46aef07 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -27,8 +27,8 @@ import { Action } from "../../dispatcher/actions"; import RoomListStore from "../../stores/room-list/RoomListStore"; import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; -import {replaceableComponent} from "../../utils/replaceableComponent"; -import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore"; +import { replaceableComponent } from "../../utils/replaceableComponent"; +import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore"; interface IProps { isMinimized: boolean; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index c0e072756a..85cc2c97e0 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; import WidgetEchoStore from '../../stores/WidgetEchoStore'; import SettingsStore from "../../settings/SettingsStore"; -import {Layout} from "../../settings/Layout"; +import { Layout } from "../../settings/Layout"; import AccessibleButton from "../views/elements/AccessibleButton"; import RightPanelStore from "../../stores/RightPanelStore"; import { haveTileForEvent } from "../views/rooms/EventTile"; @@ -54,7 +54,6 @@ import RoomContext from "../../contexts/RoomContext"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils'; import { Action } from "../../dispatcher/actions"; -import { SettingLevel } from "../../settings/SettingLevel"; import { IMatrixClientCreds } from "../../MatrixClientPeg"; import ScrollPanel from "./ScrollPanel"; import TimelinePanel from "./TimelinePanel"; @@ -63,7 +62,6 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; import ForwardMessage from "../views/rooms/ForwardMessage"; import SearchBar from "../views/rooms/SearchBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; -import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel"; import AuxPanel from "../views/rooms/AuxPanel"; import RoomHeader from "../views/rooms/RoomHeader"; import { XOR } from "../../@types/common"; @@ -82,7 +80,9 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager'; import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; import { IOpts } from "../../createRoom"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { replaceableComponent } from "../../utils/replaceableComponent"; +import { omit } from 'lodash'; +import UIStore from "../../stores/UIStore"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -155,7 +155,6 @@ export interface IState { canPeek: boolean; showApps: boolean; isPeeking: boolean; - showingPinned: boolean; showReadReceipts: boolean; showRightPanel: boolean; // error object, as from the matrix client/server API @@ -175,6 +174,7 @@ export interface IState { statusBarVisible: boolean; // We load this later by asking the js-sdk to suggest a version for us. // This object is the result of Room#getRecommendedVersion() + upgradeRecommendation?: { version: string; needsUpgrade: boolean; @@ -232,7 +232,6 @@ export default class RoomView extends React.Component { canPeek: false, showApps: false, isPeeking: false, - showingPinned: false, showReadReceipts: true, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, joining: false, @@ -327,7 +326,6 @@ export default class RoomView extends React.Component { forwardingEvent: RoomViewStore.getForwardingEvent(), // we should only peek once we have a ready client shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), - showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), wasContextSwitch: RoomViewStore.getWasContextSwitch(), }; @@ -528,7 +526,20 @@ export default class RoomView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState)); + const hasPropsDiff = objectHasDiff(this.props, nextProps); + + // React only shallow comparison and we only want to trigger + // a component re-render if a room requires an upgrade + const newUpgradeRecommendation = nextState.upgradeRecommendation || {} + + const state = omit(this.state, ['upgradeRecommendation']); + const newState = omit(nextState, ['upgradeRecommendation']) + + const hasStateDiff = + objectHasDiff(state, newState) || + (newUpgradeRecommendation.needsUpgrade === true) + + return hasPropsDiff || hasStateDiff; } componentDidUpdate() { @@ -641,6 +652,17 @@ export default class RoomView extends React.Component { SettingsStore.unwatchSetting(this.layoutWatcherRef); } + private onUserScroll = () => { + if (this.state.initialEventId && this.state.isInitialEventHighlighted) { + dis.dispatch({ + action: 'view_room', + room_id: this.state.room.roomId, + event_id: this.state.initialEventId, + highlighted: false, + }); + } + } + private onLayoutChange = () => { this.setState({ layout: SettingsStore.getValue("layout"), @@ -1114,7 +1136,8 @@ export default class RoomView extends React.Component { Promise.resolve().then(() => { const signUrl = this.props.threepidInvite?.signUrl; dis.dispatch({ - action: 'join_room', + action: Action.JoinRoom, + roomId: this.getRoomId(), opts: { inviteSignUrl: signUrl }, _type: "unknown", // TODO: instrumentation }); @@ -1375,13 +1398,6 @@ export default class RoomView extends React.Component { return ret; } - private onPinnedClick = () => { - const nowShowingPinned = !this.state.showingPinned; - const roomId = this.state.room.roomId; - this.setState({showingPinned: nowShowingPinned, searching: false}); - SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned); - }; - private onCallPlaced = (type: PlaceCallType) => { dis.dispatch({ action: 'place_call', @@ -1498,7 +1514,6 @@ export default class RoomView extends React.Component { private onSearchClick = () => { this.setState({ searching: !this.state.searching, - showingPinned: false, }); }; @@ -1511,8 +1526,10 @@ export default class RoomView extends React.Component { // jump down to the bottom of this room, where new events are arriving private jumpToLiveTimeline = () => { - this.messagePanel.jumpToLiveTimeline(); - dis.fire(Action.FocusComposer); + dis.dispatch({ + action: 'view_room', + room_id: this.state.room.roomId, + }); }; // jump up to wherever our read marker is @@ -1585,7 +1602,7 @@ export default class RoomView extends React.Component { // a maxHeight on the underlying remote video tag. // header + footer + status + give us at least 120px of scrollback at all times. - let auxPanelMaxHeight = window.innerHeight - + let auxPanelMaxHeight = UIStore.instance.windowHeight - (54 + // height of RoomHeader 36 + // height of the status area 51 + // minimum height of the message compmoser @@ -1598,33 +1615,6 @@ export default class RoomView extends React.Component { this.setState({auxPanelMaxHeight: auxPanelMaxHeight}); }; - private onFullscreenClick = () => { - dis.dispatch({ - action: 'video_fullscreen', - fullscreen: true, - }, true); - }; - - private onMuteAudioClick = () => { - const call = this.getCallForRoom(); - if (!call) { - return; - } - const newState = !call.isMicrophoneMuted(); - call.setMicrophoneMuted(newState); - this.forceUpdate(); // TODO: just update the voip buttons - }; - - private onMuteVideoClick = () => { - const call = this.getCallForRoom(); - if (!call) { - return; - } - const newState = !call.isLocalVideoMuted(); - call.setLocalVideoMuted(newState); - this.forceUpdate(); // TODO: just update the voip buttons - }; - private onStatusBarVisible = () => { if (this.unmounted) return; this.setState({ @@ -1640,24 +1630,6 @@ export default class RoomView extends React.Component { }); }; - /** - * called by the parent component when PageUp/Down/etc is pressed. - * - * We pass it down to the scroll panel. - */ - private handleScrollKey = ev => { - let panel; - if (this.searchResultsPanel.current) { - panel = this.searchResultsPanel.current; - } else if (this.messagePanel) { - panel = this.messagePanel; - } - - if (panel) { - panel.handleScrollKey(ev); - } - }; - /** * get any current call for this room */ @@ -1881,9 +1853,6 @@ export default class RoomView extends React.Component { } else if (showRoomUpgradeBar) { aux = ; hideCancel = true; - } else if (this.state.showingPinned) { - hideCancel = true; // has own cancel - aux = ; } else if (myMembership !== "join") { // We do have a room object for this room, but we're not currently in it. // We may have a 3rd party invite to it. @@ -1928,7 +1897,7 @@ export default class RoomView extends React.Component { ); } - if (SettingsStore.getValue("feature_spaces") && this.state.room?.isSpaceRoom()) { + if (this.state.room?.isSpaceRoom()) { return { eventId={this.state.initialEventId} eventPixelOffset={this.state.initialEventPixelOffset} onScroll={this.onMessageListScroll} + onUserScroll={this.onUserScroll} onReadMarkerUpdated={this.updateTopUnreadMessagesBar} showUrlPreview = {this.state.showUrlPreview} className={messagePanelClassNames} @@ -2065,6 +2035,7 @@ export default class RoomView extends React.Component { highlight={this.state.room.getUnreadNotificationCount('highlight') > 0} numUnreadMessages={this.state.numUnreadMessages} onScrollToBottomClick={this.jumpToLiveTimeline} + roomId={this.state.roomId} />); } @@ -2097,7 +2068,6 @@ export default class RoomView extends React.Component { inRoom={myMembership === 'join'} onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} - onPinnedClick={this.onPinnedClick} onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 5c5062633d..f6e1530537 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -133,6 +133,10 @@ export default class ScrollPanel extends React.Component { */ onScroll: PropTypes.func, + /* onUserScroll: callback which is called when the user interacts with the room timeline + */ + onUserScroll: PropTypes.func, + /* className: classnames to add to the top-level div */ className: PropTypes.string, @@ -535,21 +539,29 @@ export default class ScrollPanel extends React.Component { * @param {object} ev the keyboard event */ handleScrollKey = ev => { + let isScrolling = false; const roomAction = getKeyBindingsManager().getRoomAction(ev); switch (roomAction) { case RoomAction.ScrollUp: this.scrollRelative(-1); + isScrolling = true; break; case RoomAction.RoomScrollDown: this.scrollRelative(1); + isScrolling = true; break; case RoomAction.JumpToFirstMessage: this.scrollToTop(); + isScrolling = true; break; case RoomAction.JumpToLatestMessage: this.scrollToBottom(); + isScrolling = true; break; } + if (isScrolling && this.props.onUserScroll) { + this.props.onUserScroll(ev); + } }; /* Scroll the panel to bring the DOM node with the scroll token @@ -888,9 +900,8 @@ export default class ScrollPanel extends React.Component { + onWheel={this.props.onUserScroll} + className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}> { this.props.fixedChildren }
      diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 74415cc58f..8d59fe6c68 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -41,6 +41,7 @@ import TextWithTooltip from "../views/elements/TextWithTooltip"; import {useStateToggle} from "../../hooks/useStateToggle"; import {getOrder} from "../../stores/SpaceStore"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; +import {linkifyElement} from "../../HtmlUtils"; interface IHierarchyProps { space: Room; @@ -75,7 +76,7 @@ export interface ISpaceSummaryEvent { order?: string; suggested?: boolean; auto_join?: boolean; - via?: string; + via?: string[]; }; } /* eslint-enable camelcase */ @@ -100,15 +101,13 @@ const Tile: React.FC = ({ numChildRooms, children, }) => { - const name = room.name || room.canonical_alias || room.aliases?.[0] + const cli = MatrixClientPeg.get(); + const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null; + const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0] || (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room")); const [showChildren, toggleShowChildren] = useStateToggle(true); - const cli = MatrixClientPeg.get(); - const cliRoom = cli.getRoom(room.room_id); - const myMembership = cliRoom?.getMyMembership(); - const onPreviewClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -121,7 +120,7 @@ const Tile: React.FC = ({ } let button; - if (myMembership === "join") { + if (joinedRoom) { button = { _t("View") } ; @@ -145,17 +144,27 @@ const Tile: React.FC = ({ } } - let url: string; - if (room.avatar_url) { - url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20); + let avatar; + if (joinedRoom) { + avatar = ; + } else { + avatar = ; } let description = _t("%(count)s members", { count: room.num_joined_members }); - if (numChildRooms) { + if (numChildRooms !== undefined) { description += " · " + _t("%(count)s rooms", { count: numChildRooms }); } - if (room.topic) { - description += " · " + room.topic; + + const topic = joinedRoom?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || room.topic; + if (topic) { + description += " · " + topic; } let suggestedSection; @@ -166,13 +175,22 @@ const Tile: React.FC = ({ } const content = - + { avatar }
      { name } { suggestedSection }
      -
      +
      e && linkifyElement(e)} + onClick={ev => { + // prevent clicks on links from bubbling up to the room tile + if ((ev.target as HTMLElement).tagName === "A") { + ev.stopPropagation(); + } + }} + > { description }
      @@ -301,7 +319,7 @@ export const HierarchyLevel = ({ key={roomId} room={rooms.get(roomId)} numChildRooms={Array.from(relations.get(roomId)?.values() || []) - .filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length} + .filter(ev => rooms.has(ev.state_key) && !rooms.get(ev.state_key).room_type).length} suggested={relations.get(spaceId)?.get(roomId)?.content.suggested} selected={selectedMap?.get(spaceId)?.has(roomId)} onViewRoomClick={(autoJoin) => { @@ -346,9 +364,9 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev); childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id); } - if (Array.isArray(ev.content["via"])) { + if (Array.isArray(ev.content.via)) { const set = viaMap.getOrCreate(ev.state_key, new Set()); - ev.content["via"].forEach(via => set.add(via)); + ev.content.via.forEach(via => set.add(via)); } }); @@ -419,7 +437,7 @@ export const SpaceHierarchy: React.FC = ({ let content; if (roomsMap) { - const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length; + const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length; const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at let countsStr; @@ -461,8 +479,12 @@ export const SpaceHierarchy: React.FC = ({ try { for (const [parentId, childId] of selectedRelations) { await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId); - parentChildMap.get(parentId).get(childId).content = {}; - parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); + parentChildMap.get(parentId).delete(childId); + if (parentChildMap.get(parentId).size > 0) { + parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); + } else { + parentChildMap.delete(parentId); + } } } catch (e) { setError(_t("Failed to remove some rooms. Try again later")); @@ -574,7 +596,7 @@ export const SpaceHierarchy: React.FC = ({ return <> void }) => { + if (!SdkConfig.get().bug_report_endpoint_url) return null; + + return
      +
      +
      + { _t("Spaces are a beta feature.") } + { + if (onClick) onClick(); + Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, { + featureId: "feature_spaces", + }); + }}> + { _t("Feedback") } + +
      +
      ; +}; + const RoomMemberCount = ({ room, children }) => { const members = useRoomMembers(room); const count = members.length; @@ -136,15 +162,39 @@ const SpaceInfo = ({ space }) => {
      }; +const onBetaClick = () => { + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: USER_LABS_TAB, + }); +}; + const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const cli = useContext(MatrixClientContext); const myMembership = useMyRoomMembership(space); const [busy, setBusy] = useState(false); + const spacesEnabled = SettingsStore.getValue("feature_spaces"); + let inviterSection; let joinButtons; - if (myMembership === "invite") { + if (myMembership === "join") { + // XXX remove this when spaces leaves Beta + joinButtons = ( + { + dis.dispatch({ + action: "leave_room", + room_id: space.roomId, + }); + }} + > + { _t("Leave") } + + ); + } else if (myMembership === "invite") { const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender(); const inviter = inviteSender && space.getMember(inviteSender); @@ -180,6 +230,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => setBusy(true); onJoinButtonClicked(); }} + disabled={!spacesEnabled} > { _t("Accept") } @@ -192,10 +243,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => setBusy(true); onJoinButtonClicked(); }} + disabled={!spacesEnabled} > { _t("Join") } - ) + ); } if (busy) { @@ -203,6 +255,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => } return
      + { inviterSection }

      @@ -220,6 +273,20 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
      { joinButtons }
      + { !spacesEnabled &&
      + { myMembership === "join" + ? _t("To view %(spaceName)s, turn on the Spaces beta", { + spaceName: space.name, + }, { + a: sub => { sub }, + }) + : _t("To join %(spaceName)s, turn on the Spaces beta", { + spaceName: space.name, + }, { + a: sub => { sub }, + }) + } +
      }

      ; }; @@ -350,9 +417,14 @@ const SpaceLanding = ({ space }) => { { inviteButton } { settingsButton }
      -
      - -
      + + {(topic, ref) => ( +
      + { topic } +
      + )} +
      +
      { const [error, setError] = useState(""); const numFields = 3; const placeholders = [_t("General"), _t("Random"), _t("Support")]; - // TODO vary default prefills for "Just Me" spaces const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]); const fields = new Array(numFields).fill(0).map((_, i) => { const name = "roomName" + i; @@ -382,14 +453,18 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { value={roomNames[i]} onChange={ev => setRoomName(i, ev.target.value)} autoFocus={i === 2} + disabled={busy} />; }); - const onNextClick = async () => { + const onNextClick = async (ev) => { + ev.preventDefault(); + if (busy) return; setError(""); setBusy(true); try { - await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => { + const filteredRoomNames = roomNames.map(name => name.trim()).filter(Boolean); + await Promise.all(filteredRoomNames.map(name => { return createRoom({ createOpts: { preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat, @@ -402,7 +477,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { parentSpace: space, }); })); - onFinished(); + onFinished(filteredRoomNames.length > 0); } catch (e) { console.error("Failed to create initial space rooms", e); setError(_t("Failed to create initial space rooms")); @@ -410,7 +485,10 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { setBusy(false); }; - let onClick = onFinished; + let onClick = (ev) => { + ev.preventDefault(); + onFinished(false); + }; let buttonLabel = _t("Skip for now"); if (roomNames.some(name => name.trim())) { onClick = onNextClick; @@ -422,54 +500,26 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
      { description }
      { error &&
      { error }
      } - { fields } +
      + { fields } +
      - { buttonLabel } - + element="input" + type="submit" + form="mx_SpaceSetupFirstRooms" + value={buttonLabel} + />
      +
    ; }; const SpaceAddExistingRooms = ({ space, onFinished }) => { - const [selectedToAdd, setSelectedToAdd] = useState(new Set()); - - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); - - let onClick = onFinished; - let buttonLabel = _t("Skip for now"); - if (selectedToAdd.size > 0) { - onClick = async () => { - setBusy(true); - - for (const room of selectedToAdd) { - const via = calculateRoomVia(room); - try { - await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { - if (e.errcode === "M_LIMIT_EXCEEDED") { - await sleep(e.data.retry_after_ms); - return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry - } - - throw e; - }); - } catch (e) { - console.error("Failed to add rooms to space", e); - setError(_t("Failed to add rooms to space")); - break; - } - } - setBusy(false); - }; - buttonLabel = busy ? _t("Adding...") : _t("Add"); - } - return

    { _t("What do you want to organise?") }

    @@ -477,36 +527,28 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => { "no one will be informed. You can add more later.") }
    - { error &&
    { error }
    } - { - if (checked) { - selectedToAdd.add(room); - } else { - selectedToAdd.delete(room); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} + emptySelectionButton={ + + { _t("Skip for now") } + + } + onFinished={onFinished} />
    - - { buttonLabel } - +
    +
    ; }; -const SpaceSetupPublicShare = ({ space, onFinished }) => { +const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRooms }) => { return
    -

    { _t("Share %(name)s", { name: space.name }) }

    +

    { _t("Share %(name)s", { + name: justCreatedOpts?.createOpts?.name || space.name, + }) }

    { _t("It's just you at the moment, it will be even better with others.") }
    @@ -515,17 +557,20 @@ const SpaceSetupPublicShare = ({ space, onFinished }) => {
    - { _t("Go to my first room") } + { createdRooms ? _t("Go to my first room") : _t("Go to my space") }
    +
    ; }; -const SpaceSetupPrivateScope = ({ space, onFinished }) => { +const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => { return

    { _t("Who are you working with?") }

    - { _t("Make sure the right people have access to %(name)s", { name: space.name }) } + { _t("Make sure the right people have access to %(name)s", { + name: justCreatedOpts?.createOpts?.name || space.name, + }) }
    {

    { _t("Me and my teammates") }

    { _t("A private space for you and your teammates") }
    +
    ; }; @@ -572,10 +618,13 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => { ref={fieldRefs[i]} onValidate={validateEmailRules} autoFocus={i === 0} + disabled={busy} />; }); - const onNextClick = async () => { + const onNextClick = async (ev) => { + ev.preventDefault(); + if (busy) return; setError(""); for (let i = 0; i < fieldRefs.length; i++) { const fieldRef = fieldRefs[i]; @@ -609,7 +658,10 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => { setBusy(false); }; - let onClick = onFinished; + let onClick = (ev) => { + ev.preventDefault(); + onFinished(); + }; let buttonLabel = _t("Skip for now"); if (emailAddresses.some(name => name.trim())) { onClick = onNextClick; @@ -622,8 +674,21 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => { { _t("Make sure the right people have access. You can invite more later.") }
    +
    + + { _t("This is an experimental feature. For now, " + + "new users receiving an invite will have to open the invite on to actually join.", {}, { + b: sub => { sub }, + link: () => + app.element.io + , + }) } +
    + { error &&
    { error }
    } - { fields } +
    + { fields } +
    {
    - - { buttonLabel } - +
    +
    ; }; @@ -737,7 +809,7 @@ export default class SpaceRoomView extends React.PureComponent { let suggestedRooms = SpaceStore.instance.suggestedRooms; if (SpaceStore.instance.activeSpace !== this.props.space) { // the space store has the suggested rooms loaded for a different space, fetch the right ones - suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)).rooms; + suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)); } if (suggestedRooms.length) { @@ -745,9 +817,11 @@ export default class SpaceRoomView extends React.PureComponent { defaultDispatcher.dispatch({ action: "view_room", room_id: room.room_id, + room_alias: room.canonical_alias || room.aliases?.[0], + via_servers: room.viaServers, oobData: { avatarUrl: room.avatar_url, - name: room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"), + name: room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room"), }, }); return; @@ -759,7 +833,7 @@ export default class SpaceRoomView extends React.PureComponent { private renderBody() { switch (this.state.phase) { case Phase.Landing: - if (this.state.myMembership === "join") { + if (this.state.myMembership === "join" && SettingsStore.getValue("feature_spaces")) { return ; } else { return { return this.setState({ phase: Phase.PublicShare })} + onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.PublicShare, createdRooms })} />; case Phase.PublicShare: - return ; + return ; case Phase.PrivateScope: return { this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateExistingRooms }); }} @@ -801,7 +881,7 @@ export default class SpaceRoomView extends React.PureComponent { title={_t("What projects are you working on?")} description={_t("We'll create rooms for each of them. " + "You can add more later too, including already existing ones.")} - onFinished={() => this.setState({ phase: Phase.Landing })} + onFinished={(createdRooms: boolean) => this.setState({ phase: Phase.Landing, createdRooms })} />; case Phase.PrivateExistingRooms: return { + const client = MatrixClientPeg.get(); + client.decryptEventIfNeeded(event); + }); + const firstVisibleEventIndex = this._checkForPreJoinUISI(events); // Hold onto the live events separately. The read receipt and read marker @@ -1444,6 +1436,7 @@ class TimelinePanel extends React.Component { ourUserId={MatrixClientPeg.get().credentials.userId} stickyBottom={stickyBottom} onScroll={this.onMessageListScroll} + onUserScroll={this.props.onUserScroll} onFillRequest={this.onMessageListFillRequest} onUnfillRequest={this.onMessageListUnfillRequest} isTwelveHour={this.state.isTwelveHour} diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 1fd3e3419f..273c8a079f 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -55,6 +55,7 @@ export default class ToastContainer extends React.Component<{}, IState> { const totalCount = this.state.toasts.length; const isStacked = totalCount > 1; let toast; + let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; const {title, icon, key, component, className, props} = topToast; @@ -79,16 +80,17 @@ export default class ToastContainer extends React.Component<{}, IState> {
    {React.createElement(component, toastProps)}
    ); + + containerClasses = classNames("mx_ToastContainer", { + "mx_ToastContainer_stacked": isStacked, + }); } - - const containerClasses = classNames("mx_ToastContainer", { - "mx_ToastContainer_stacked": isStacked, - }); - - return ( -
    - {toast} -
    - ); + return toast + ? ( +
    + {toast} +
    + ) + : null; } } diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 65861624e6..fb4829f879 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -57,7 +57,8 @@ import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes"; import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore"; import RoomName from "../views/elements/RoomName"; import {replaceableComponent} from "../../utils/replaceableComponent"; - +import InlineSpinner from "../views/elements/InlineSpinner"; +import TooltipButton from "../views/elements/TooltipButton"; interface IProps { isMinimized: boolean; } @@ -68,6 +69,7 @@ interface IState { contextMenuPosition: PartialDOMRect; isDarkTheme: boolean; selectedSpace?: Room; + pendingRoomJoin: Set; } @replaceableComponent("structures.UserMenu") @@ -84,6 +86,7 @@ export default class UserMenu extends React.Component { this.state = { contextMenuPosition: null, isDarkTheme: this.isUserOnDarkTheme(), + pendingRoomJoin: new Set(), }; OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); @@ -103,6 +106,7 @@ export default class UserMenu extends React.Component { this.dispatcherRef = defaultDispatcher.register(this.onAction); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate); + MatrixClientPeg.get().on("Room", this.onRoom); } public componentWillUnmount() { @@ -114,6 +118,11 @@ export default class UserMenu extends React.Component { if (SettingsStore.getValue("feature_spaces")) { SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); } + MatrixClientPeg.get().removeListener("Room", this.onRoom); + } + + private onRoom = (room: Room): void => { + this.removePendingJoinRoom(room.roomId); } private onTagStoreUpdate = () => { @@ -147,15 +156,39 @@ export default class UserMenu extends React.Component { }; private onAction = (ev: ActionPayload) => { - if (ev.action !== Action.ToggleUserMenu) return; // not interested - - if (this.state.contextMenuPosition) { - this.setState({contextMenuPosition: null}); - } else { - if (this.buttonRef.current) this.buttonRef.current.click(); + switch (ev.action) { + case Action.ToggleUserMenu: + if (this.state.contextMenuPosition) { + this.setState({contextMenuPosition: null}); + } else { + if (this.buttonRef.current) this.buttonRef.current.click(); + } + break; + case Action.JoinRoom: + this.addPendingJoinRoom(ev.roomId); + break; + case Action.JoinRoomReady: + case Action.JoinRoomError: + this.removePendingJoinRoom(ev.roomId); + break; } }; + private addPendingJoinRoom(roomId: string): void { + this.setState({ + pendingRoomJoin: new Set(this.state.pendingRoomJoin) + .add(roomId), + }); + } + + private removePendingJoinRoom(roomId: string): void { + if (this.state.pendingRoomJoin.delete(roomId)) { + this.setState({ + pendingRoomJoin: new Set(this.state.pendingRoomJoin), + }) + } + } + private onOpenMenuClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -617,6 +650,14 @@ export default class UserMenu extends React.Component { /> {name} + {this.state.pendingRoomJoin.size > 0 && ( + + + + )} {dnd} {buttons} diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 34a5410928..d34582b0c3 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -59,6 +59,7 @@ interface IProps { fallbackHsUrl?: string; defaultDeviceDisplayName?: string; fragmentAfterLogin?: string; + defaultUsername?: string; // Called when the user has logged in. Params: // - The object returned by the login API @@ -119,7 +120,7 @@ export default class LoginComponent extends React.PureComponent flows: null, - username: "", + username: props.defaultUsername? props.defaultUsername: '', phoneCountry: null, phoneNumber: "", diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 96fb9bdc82..6feb1e34f7 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -61,7 +61,7 @@ interface IProps { is_url?: string; session_id: string; /* eslint-enable camelcase */ - }): void; + }): string; // registration shouldn't know or care how login is done. onLoginClick(): void; onServerConfigChange(config: ValidatedServerConfig): void; @@ -223,7 +223,8 @@ export default class Registration extends React.Component { this.setState({ flows: e.data.flows, }); - } else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") { + } else if (e.httpStatus === 403 || e.errcode === "M_FORBIDDEN") { + // Check for 403 or M_FORBIDDEN, Synapse used to send 403 M_UNKNOWN but now sends 403 M_FORBIDDEN. // At this point registration is pretty much disabled, but before we do that let's // quickly check to see if the server supports SSO instead. If it does, we'll send // the user off to the login page to figure their account out. @@ -467,7 +468,7 @@ export default class Registration extends React.Component { let ssoSection; if (this.state.ssoFlow) { let continueWithSection; - const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"] || []; + const providers = this.state.ssoFlow.identity_providers || []; // when there is only a single (or 0) providers we show a wide button with `Continue with X` text if (providers.length > 1) { // i18n: ssoButtons is a placeholder to help translators understand context diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.tsx similarity index 72% rename from src/components/views/auth/InteractiveAuthEntryComponents.js rename to src/components/views/auth/InteractiveAuthEntryComponents.tsx index e34349c474..e819e1e59c 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -1,7 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2016-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; +import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react'; +import classNames from 'classnames'; +import { MatrixClient } from "matrix-js-sdk/src/client"; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -27,6 +25,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import Spinner from "../elements/Spinner"; import CountlyAnalytics from "../../../CountlyAnalytics"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { LocalisedPolicy, Policies } from '../../../Terms'; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -74,36 +73,72 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; * focus: set the input focus appropriately in the form. */ +enum AuthType { + Password = "m.login.password", + Recaptcha = "m.login.recaptcha", + Terms = "m.login.terms", + Email = "m.login.email.identity", + Msisdn = "m.login.msisdn", + Sso = "m.login.sso", + SsoUnstable = "org.matrix.login.sso", +} + +/* eslint-disable camelcase */ +interface IAuthDict { + type?: AuthType; + // TODO: Remove `user` once servers support proper UIA + // See https://github.com/vector-im/element-web/issues/10312 + user?: string; + identifier?: any; + password?: string; + response?: string; + // TODO: Remove `threepid_creds` once servers support proper UIA + // See https://github.com/vector-im/element-web/issues/10312 + // See https://github.com/matrix-org/matrix-doc/issues/2220 + threepid_creds?: any; + threepidCreds?: any; +} +/* eslint-enable camelcase */ + export const DEFAULT_PHASE = 0; -@replaceableComponent("views.auth.PasswordAuthEntry") -export class PasswordAuthEntry extends React.Component { - static LOGIN_TYPE = "m.login.password"; +interface IAuthEntryProps { + matrixClient: MatrixClient; + loginType: string; + authSessionId: string; + errorText?: string; + // Is the auth logic currently waiting for something to happen? + busy?: boolean; + onPhaseChange: (phase: number) => void; + submitAuthDict: (auth: IAuthDict) => void; +} - static propTypes = { - matrixClient: PropTypes.object.isRequired, - submitAuthDict: PropTypes.func.isRequired, - errorText: PropTypes.string, - // is the auth logic currently waiting for something to - // happen? - busy: PropTypes.bool, - onPhaseChange: PropTypes.func.isRequired, - }; +interface IPasswordAuthEntryState { + password: string; +} + +@replaceableComponent("views.auth.PasswordAuthEntry") +export class PasswordAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Password; + + constructor(props) { + super(props); + + this.state = { + password: "", + }; + } componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); } - state = { - password: "", - }; - - _onSubmit = e => { + private onSubmit = (e: FormEvent) => { e.preventDefault(); if (this.props.busy) return; this.props.submitAuthDict({ - type: PasswordAuthEntry.LOGIN_TYPE, + type: AuthType.Password, // TODO: Remove `user` once servers support proper UIA // See https://github.com/vector-im/element-web/issues/10312 user: this.props.matrixClient.credentials.userId, @@ -115,7 +150,7 @@ export class PasswordAuthEntry extends React.Component { }); }; - _onPasswordFieldChange = ev => { + private onPasswordFieldChange = (ev: ChangeEvent) => { // enable the submit button iff the password is non-empty this.setState({ password: ev.target.value, @@ -123,7 +158,7 @@ export class PasswordAuthEntry extends React.Component { }; render() { - const passwordBoxClass = classnames({ + const passwordBoxClass = classNames({ "error": this.props.errorText, }); @@ -155,7 +190,7 @@ export class PasswordAuthEntry extends React.Component { return (

    { _t("Confirm your identity by entering your account password below.") }

    -
    +
    { submitButtonOrSpinner } @@ -175,26 +210,26 @@ export class PasswordAuthEntry extends React.Component { } } -@replaceableComponent("views.auth.RecaptchaAuthEntry") -export class RecaptchaAuthEntry extends React.Component { - static LOGIN_TYPE = "m.login.recaptcha"; - - static propTypes = { - submitAuthDict: PropTypes.func.isRequired, - stageParams: PropTypes.object.isRequired, - errorText: PropTypes.string, - busy: PropTypes.bool, - onPhaseChange: PropTypes.func.isRequired, +/* eslint-disable camelcase */ +interface IRecaptchaAuthEntryProps extends IAuthEntryProps { + stageParams?: { + public_key?: string; }; +} +/* eslint-enable camelcase */ + +@replaceableComponent("views.auth.RecaptchaAuthEntry") +export class RecaptchaAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Recaptcha; componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); } - _onCaptchaResponse = response => { + private onCaptchaResponse = (response: string) => { CountlyAnalytics.instance.track("onboarding_grecaptcha_submit"); this.props.submitAuthDict({ - type: RecaptchaAuthEntry.LOGIN_TYPE, + type: AuthType.Recaptcha, response: response, }); }; @@ -230,7 +265,7 @@ export class RecaptchaAuthEntry extends React.Component { return (
    { errorSection }
    @@ -238,18 +273,28 @@ export class RecaptchaAuthEntry extends React.Component { } } -@replaceableComponent("views.auth.TermsAuthEntry") -export class TermsAuthEntry extends React.Component { - static LOGIN_TYPE = "m.login.terms"; - - static propTypes = { - submitAuthDict: PropTypes.func.isRequired, - stageParams: PropTypes.object.isRequired, - errorText: PropTypes.string, - busy: PropTypes.bool, - showContinue: PropTypes.bool, - onPhaseChange: PropTypes.func.isRequired, +interface ITermsAuthEntryProps extends IAuthEntryProps { + stageParams?: { + policies?: Policies; }; + showContinue: boolean; +} + +interface LocalisedPolicyWithId extends LocalisedPolicy { + id: string; +} + +interface ITermsAuthEntryState { + policies: LocalisedPolicyWithId[]; + toggledPolicies: { + [policy: string]: boolean; + }; + errorText?: string; +} + +@replaceableComponent("views.auth.TermsAuthEntry") +export class TermsAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Terms; constructor(props) { super(props); @@ -294,8 +339,11 @@ export class TermsAuthEntry extends React.Component { initToggles[policyId] = false; - langPolicy.id = policyId; - pickedPolicies.push(langPolicy); + pickedPolicies.push({ + id: policyId, + name: langPolicy.name, + url: langPolicy.url, + }); } this.state = { @@ -311,11 +359,11 @@ export class TermsAuthEntry extends React.Component { this.props.onPhaseChange(DEFAULT_PHASE); } - tryContinue = () => { - this._trySubmit(); + public tryContinue = () => { + this.trySubmit(); }; - _togglePolicy(policyId) { + private togglePolicy(policyId: string) { const newToggles = {}; for (const policy of this.state.policies) { let checked = this.state.toggledPolicies[policy.id]; @@ -326,7 +374,7 @@ export class TermsAuthEntry extends React.Component { this.setState({"toggledPolicies": newToggles}); } - _trySubmit = () => { + private trySubmit = () => { let allChecked = true; for (const policy of this.state.policies) { const checked = this.state.toggledPolicies[policy.id]; @@ -334,7 +382,7 @@ export class TermsAuthEntry extends React.Component { } if (allChecked) { - this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE}); + this.props.submitAuthDict({type: AuthType.Terms}); CountlyAnalytics.instance.track("onboarding_terms_complete"); } else { this.setState({errorText: _t("Please review and accept all of the homeserver's policies")}); @@ -356,7 +404,7 @@ export class TermsAuthEntry extends React.Component { checkboxes.push( // XXX: replace with StyledCheckbox , ); @@ -375,7 +423,7 @@ export class TermsAuthEntry extends React.Component { if (this.props.showContinue !== false) { // XXX: button classes submitButton = ; + onClick={this.trySubmit} disabled={!allChecked}>{_t("Accept")}; } return ( @@ -389,21 +437,18 @@ export class TermsAuthEntry extends React.Component { } } -@replaceableComponent("views.auth.EmailIdentityAuthEntry") -export class EmailIdentityAuthEntry extends React.Component { - static LOGIN_TYPE = "m.login.email.identity"; - - static propTypes = { - matrixClient: PropTypes.object.isRequired, - submitAuthDict: PropTypes.func.isRequired, - authSessionId: PropTypes.string.isRequired, - clientSecret: PropTypes.string.isRequired, - inputs: PropTypes.object.isRequired, - stageState: PropTypes.object.isRequired, - fail: PropTypes.func.isRequired, - setEmailSid: PropTypes.func.isRequired, - onPhaseChange: PropTypes.func.isRequired, +interface IEmailIdentityAuthEntryProps extends IAuthEntryProps { + inputs?: { + emailAddress?: string; }; + stageState?: { + emailSid: string; + }; +} + +@replaceableComponent("views.auth.EmailIdentityAuthEntry") +export class EmailIdentityAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Email; componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); @@ -427,7 +472,7 @@ export class EmailIdentityAuthEntry extends React.Component { return (

    { _t("A confirmation email has been sent to %(emailAddress)s", - { emailAddress: (sub) => { this.props.inputs.emailAddress } }, + { emailAddress: { this.props.inputs.emailAddress } }, ) }

    { _t("Open the link in the email to continue registration.") }

    @@ -437,37 +482,44 @@ export class EmailIdentityAuthEntry extends React.Component { } } +interface IMsisdnAuthEntryProps extends IAuthEntryProps { + inputs: { + phoneCountry: string; + phoneNumber: string; + }; + clientSecret: string; + fail: (error: Error) => void; +} + +interface IMsisdnAuthEntryState { + token: string; + requestingToken: boolean; + errorText: string; +} + @replaceableComponent("views.auth.MsisdnAuthEntry") -export class MsisdnAuthEntry extends React.Component { - static LOGIN_TYPE = "m.login.msisdn"; +export class MsisdnAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Msisdn; - static propTypes = { - inputs: PropTypes.shape({ - phoneCountry: PropTypes.string, - phoneNumber: PropTypes.string, - }), - fail: PropTypes.func, - clientSecret: PropTypes.func, - submitAuthDict: PropTypes.func.isRequired, - matrixClient: PropTypes.object, - onPhaseChange: PropTypes.func.isRequired, - }; + private submitUrl: string; + private sid: string; + private msisdn: string; - state = { - token: '', - requestingToken: false, - }; + constructor(props) { + super(props); + + this.state = { + token: '', + requestingToken: false, + errorText: '', + }; + } componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); - this._submitUrl = null; - this._sid = null; - this._msisdn = null; - this._tokenBox = null; - this.setState({requestingToken: true}); - this._requestMsisdnToken().catch((e) => { + this.requestMsisdnToken().catch((e) => { this.props.fail(e); }).finally(() => { this.setState({requestingToken: false}); @@ -477,26 +529,26 @@ export class MsisdnAuthEntry extends React.Component { /* * Requests a verification token by SMS. */ - _requestMsisdnToken() { + private requestMsisdnToken(): Promise { return this.props.matrixClient.requestRegisterMsisdnToken( this.props.inputs.phoneCountry, this.props.inputs.phoneNumber, this.props.clientSecret, 1, // TODO: Multiple send attempts? ).then((result) => { - this._submitUrl = result.submit_url; - this._sid = result.sid; - this._msisdn = result.msisdn; + this.submitUrl = result.submit_url; + this.sid = result.sid; + this.msisdn = result.msisdn; }); } - _onTokenChange = e => { + private onTokenChange = (e: ChangeEvent) => { this.setState({ token: e.target.value, }); }; - _onFormSubmit = async e => { + private onFormSubmit = async (e: FormEvent) => { e.preventDefault(); if (this.state.token == '') return; @@ -506,20 +558,20 @@ export class MsisdnAuthEntry extends React.Component { try { let result; - if (this._submitUrl) { + if (this.submitUrl) { result = await this.props.matrixClient.submitMsisdnTokenOtherUrl( - this._submitUrl, this._sid, this.props.clientSecret, this.state.token, + this.submitUrl, this.sid, this.props.clientSecret, this.state.token, ); } else { throw new Error("The registration with MSISDN flow is misconfigured"); } if (result.success) { const creds = { - sid: this._sid, + sid: this.sid, client_secret: this.props.clientSecret, }; this.props.submitAuthDict({ - type: MsisdnAuthEntry.LOGIN_TYPE, + type: AuthType.Msisdn, // TODO: Remove `threepid_creds` once servers support proper UIA // See https://github.com/vector-im/element-web/issues/10312 // See https://github.com/matrix-org/matrix-doc/issues/2220 @@ -543,7 +595,7 @@ export class MsisdnAuthEntry extends React.Component { return ; } else { const enableSubmit = Boolean(this.state.token); - const submitClasses = classnames({ + const submitClasses = classNames({ mx_InteractiveAuthEntryComponents_msisdnSubmit: true, mx_GeneralButton: true, }); @@ -558,16 +610,16 @@ export class MsisdnAuthEntry extends React.Component { return (

    { _t("A text message has been sent to %(msisdn)s", - { msisdn: { this._msisdn } }, + { msisdn: { this.msisdn } }, ) }

    { _t("Please enter the code it contains:") }

    - +
    @@ -584,40 +636,40 @@ export class MsisdnAuthEntry extends React.Component { } } -@replaceableComponent("views.auth.SSOAuthEntry") -export class SSOAuthEntry extends React.Component { - static propTypes = { - matrixClient: PropTypes.object.isRequired, - authSessionId: PropTypes.string.isRequired, - loginType: PropTypes.string.isRequired, - submitAuthDict: PropTypes.func.isRequired, - errorText: PropTypes.string, - onPhaseChange: PropTypes.func.isRequired, - continueText: PropTypes.string, - continueKind: PropTypes.string, - onCancel: PropTypes.func, - }; +interface ISSOAuthEntryProps extends IAuthEntryProps { + continueText?: string; + continueKind?: string; + onCancel?: () => void; +} - static LOGIN_TYPE = "m.login.sso"; - static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso"; +interface ISSOAuthEntryState { + phase: number; + attemptFailed: boolean; +} + +@replaceableComponent("views.auth.SSOAuthEntry") +export class SSOAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Sso; + static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable; static PHASE_PREAUTH = 1; // button to start SSO static PHASE_POSTAUTH = 2; // button to confirm SSO completed - _ssoUrl: string; + private ssoUrl: string; + private popupWindow: Window; constructor(props) { super(props); // We actually send the user through fallback auth so we don't have to // deal with a redirect back to us, losing application context. - this._ssoUrl = props.matrixClient.getFallbackAuthUrl( + this.ssoUrl = props.matrixClient.getFallbackAuthUrl( this.props.loginType, this.props.authSessionId, ); - this._popupWindow = null; - window.addEventListener("message", this._onReceiveMessage); + this.popupWindow = null; + window.addEventListener("message", this.onReceiveMessage); this.state = { phase: SSOAuthEntry.PHASE_PREAUTH, @@ -625,44 +677,44 @@ export class SSOAuthEntry extends React.Component { }; } - componentDidMount(): void { + componentDidMount() { this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH); } componentWillUnmount() { - window.removeEventListener("message", this._onReceiveMessage); - if (this._popupWindow) { - this._popupWindow.close(); - this._popupWindow = null; + window.removeEventListener("message", this.onReceiveMessage); + if (this.popupWindow) { + this.popupWindow.close(); + this.popupWindow = null; } } - attemptFailed = () => { + public attemptFailed = () => { this.setState({ attemptFailed: true, }); }; - _onReceiveMessage = event => { + private onReceiveMessage = (event: MessageEvent) => { if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) { - if (this._popupWindow) { - this._popupWindow.close(); - this._popupWindow = null; + if (this.popupWindow) { + this.popupWindow.close(); + this.popupWindow = null; } } }; - onStartAuthClick = () => { + private onStartAuthClick = () => { // Note: We don't use PlatformPeg's startSsoAuth functions because we almost // certainly will need to open the thing in a new tab to avoid losing application // context. - this._popupWindow = window.open(this._ssoUrl, "_blank"); + this.popupWindow = window.open(this.ssoUrl, "_blank"); this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH}); this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH); }; - onConfirmClick = () => { + private onConfirmClick = () => { this.props.submitAuthDict({}); }; @@ -716,46 +768,37 @@ export class SSOAuthEntry extends React.Component { } @replaceableComponent("views.auth.FallbackAuthEntry") -export class FallbackAuthEntry extends React.Component { - static propTypes = { - matrixClient: PropTypes.object.isRequired, - authSessionId: PropTypes.string.isRequired, - loginType: PropTypes.string.isRequired, - submitAuthDict: PropTypes.func.isRequired, - errorText: PropTypes.string, - onPhaseChange: PropTypes.func.isRequired, - }; +export class FallbackAuthEntry extends React.Component { + private popupWindow: Window; + private fallbackButton = createRef(); constructor(props) { super(props); // we have to make the user click a button, as browsers will block // the popup if we open it immediately. - this._popupWindow = null; - window.addEventListener("message", this._onReceiveMessage); - - this._fallbackButton = createRef(); + this.popupWindow = null; + window.addEventListener("message", this.onReceiveMessage); } - componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); } componentWillUnmount() { - window.removeEventListener("message", this._onReceiveMessage); - if (this._popupWindow) { - this._popupWindow.close(); + window.removeEventListener("message", this.onReceiveMessage); + if (this.popupWindow) { + this.popupWindow.close(); } } - focus = () => { - if (this._fallbackButton.current) { - this._fallbackButton.current.focus(); + public focus = () => { + if (this.fallbackButton.current) { + this.fallbackButton.current.focus(); } }; - _onShowFallbackClick = e => { + private onShowFallbackClick = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -763,10 +806,10 @@ export class FallbackAuthEntry extends React.Component { this.props.loginType, this.props.authSessionId, ); - this._popupWindow = window.open(url, "_blank"); + this.popupWindow = window.open(url, "_blank"); }; - _onReceiveMessage = event => { + private onReceiveMessage = (event: MessageEvent) => { if ( event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl() @@ -786,27 +829,31 @@ export class FallbackAuthEntry extends React.Component { } return ( ); } } -const AuthEntryComponents = [ - PasswordAuthEntry, - RecaptchaAuthEntry, - EmailIdentityAuthEntry, - MsisdnAuthEntry, - TermsAuthEntry, - SSOAuthEntry, -]; - -export default function getEntryComponentForLoginType(loginType) { - for (const c of AuthEntryComponents) { - if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) { - return c; - } +export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component { + switch (loginType) { + case AuthType.Password: + return PasswordAuthEntry; + case AuthType.Recaptcha: + return RecaptchaAuthEntry; + case AuthType.Email: + return EmailIdentityAuthEntry; + case AuthType.Msisdn: + return MsisdnAuthEntry; + case AuthType.Terms: + return TermsAuthEntry; + case AuthType.Sso: + case AuthType.SsoUnstable: + return SSOAuthEntry; + default: + return FallbackAuthEntry; } - return FallbackAuthEntry; } diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 5ecdd4ec5a..8ce05e0a55 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -179,7 +179,7 @@ const BaseAvatar = (props: IProps) => { width: toPx(width), height: toPx(height), }} - title={title} alt="" + title={title} alt={_t("Avatar")} inputRef={inputRef} {...otherProps} /> ); diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index e95022687a..42aef24086 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -20,7 +20,6 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { User } from "matrix-js-sdk/src/models/user"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { TagID } from '../../../stores/room-list/models'; import RoomAvatar from "./RoomAvatar"; import NotificationBadge from '../rooms/NotificationBadge'; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; @@ -35,7 +34,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; interface IProps { room: Room; avatarSize: number; - tag: TagID; displayBadge?: boolean; forceCount?: boolean; oobData?: object; @@ -121,7 +119,10 @@ export default class DecoratedRoomAvatar extends React.PureComponent void }) => { + if (onClick) { + return +
    + { _t("Spaces is a beta feature") } +
    +
    + { _t("Tap for more info") } +
    +
    } + onClick={onClick} + tooltipProps={{ yOffset: -10 }} + > + { _t("Beta") } + ; + } + + return + { _t("Beta") } + ; +}; + +const BetaCard = ({ title: titleOverride, featureId }: IProps) => { + const info = SettingsStore.getBetaInfo(featureId); + if (!info) return null; // Beta is invalid/disabled + + const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info; + const value = SettingsStore.getValue(featureId); + + let feedbackButton; + if (value && feedbackLabel && feedbackSubheading && SdkConfig.get().bug_report_endpoint_url) { + feedbackButton = { + Modal.createTrackedDialog("Beta Feedback", featureId, BetaFeedbackDialog, { featureId }); + }} + kind="primary" + > + { _t("Feedback") } + ; + } + + return
    +
    +

    + { titleOverride || _t(title) } + +

    + { _t(caption) } +
    + { feedbackButton } + SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)} + kind={feedbackButton ? "primary_outline" : "primary"} + > + { value ? _t("Leave the beta") : _t("Join the beta") } + +
    + { disclaimer &&
    + { disclaimer(value) } +
    } +
    + +
    ; +}; + +export default BetaCard; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 365f2ab1de..594b98b1f5 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -17,9 +17,9 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {EventStatus} from 'matrix-js-sdk/src/models/event'; +import { EventStatus } from 'matrix-js-sdk/src/models/event'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -28,9 +28,10 @@ import Resend from '../../../Resend'; import SettingsStore from '../../../settings/SettingsStore'; import { isUrlPermitted } from '../../../HtmlUtils'; import { isContentActionable } from '../../../utils/EventUtils'; -import {MenuItem} from "../../structures/ContextMenu"; -import {EventType} from "matrix-js-sdk/src/@types/event"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { MenuItem } from "../../structures/ContextMenu"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -82,7 +83,7 @@ export default class MessageContextMenu extends React.Component { const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId) && this.props.mxEvent.getType() !== EventType.RoomServerAcl && this.props.mxEvent.getType() !== EventType.RoomEncryption; - let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli); + let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli); // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality if (!SettingsStore.getValue("feature_pinning")) canPin = false; @@ -92,7 +93,7 @@ export default class MessageContextMenu extends React.Component { _isPinned() { const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', ''); + const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ''); if (!pinnedEvent) return false; const content = pinnedEvent.getContent(); return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); @@ -165,25 +166,23 @@ export default class MessageContextMenu extends React.Component { }; onPinClick = () => { - MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '') - .catch((e) => { - // Intercept the Event Not Found error and fall through the promise chain with no event. - if (e.errcode === "M_NOT_FOUND") return null; - throw e; - }) - .then((event) => { - const eventIds = (event ? event.pinned : []) || []; - if (!eventIds.includes(this.props.mxEvent.getId())) { - // Not pinned - add - eventIds.push(this.props.mxEvent.getId()); - } else { - // Pinned - remove - eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1); - } + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(this.props.mxEvent.getRoomId()); + const eventId = this.props.mxEvent.getId(); - const cli = MatrixClientPeg.get(); - cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, ''); + const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || []; + if (pinnedIds.includes(eventId)) { + pinnedIds.splice(pinnedIds.indexOf(eventId), 1); + } else { + pinnedIds.push(eventId); + cli.setRoomAccountData(room.roomId, ReadPinsEventId, { + event_ids: [ + ...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids, + eventId, + ], }); + } + cli.sendStateEvent(this.props.mxEvent.getRoomId(), EventType.RoomPinnedEvents, { pinned: pinnedIds }, ""); this.closeMenu(); }; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index a33248200c..822ffc2827 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useContext, useMemo, useState} from "react"; +import React, {ReactNode, useContext, useMemo, useState} from "react"; import classNames from "classnames"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClient} from "matrix-js-sdk/src/client"; @@ -36,6 +36,9 @@ import StyledCheckbox from "../elements/StyledCheckbox"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import ProgressBar from "../elements/ProgressBar"; +import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; +import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; +import QueryMatcher from "../../../autocomplete/QueryMatcher"; interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -45,7 +48,10 @@ interface IProps extends IDialogProps { const Entry = ({ room, checked, onChange }) => { return
    ; } else { + let button = emptySelectionButton; + if (!button || selectedToAdd.size > 0) { + button = + { _t("Add") } + ; + } + footer = <> -
    { _t("Want to add a new room instead?") }
    - onCreateRoomClick(cli, space)} kind="link"> - { _t("Create a new room") } - + { footerPrompt }
    - - { _t("Add") } - + { button } ; } + const onChange = !busy && !error ? (checked, room) => { + if (checked) { + selectedToAdd.add(room); + } else { + selectedToAdd.delete(room); + } + setSelectedToAdd(new Set(selectedToAdd)); + } : null; + + return
    + + + { rooms.length > 0 ? ( +
    +

    { _t("Rooms") }

    + { rooms.map(room => { + return { + onChange(checked, room); + } : null} + />; + }) } +
    + ) : undefined } + + { spaces.length > 0 ? ( +
    +

    { _t("Spaces") }

    +
    +
    { _t("Feeling experimental?") }
    +
    { _t("You can add existing spaces to a space.") }
    +
    + { spaces.map(space => { + return { + onChange(checked, space); + } : null} + />; + }) } +
    + ) : null } + + { dms.length > 0 ? ( +
    +

    { _t("Direct Messages") }

    + { dms.map(room => { + return { + onChange(checked, room); + } : null} + />; + }) } +
    + ) : null } + + { spaces.length + rooms.length + dms.length < 1 ? + { _t("No results") } + : undefined } +
    + +
    + { footer } +
    +
    ; +}; + +const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { + const [selectedSpace, setSelectedSpace] = useState(space); + const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); + + let spaceOptionSection; + if (existingSubspaces.length > 0) { + const options = [space, ...existingSubspaces].map((space) => { + const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", { + mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace, + }); + return
    + + { space.name || getDisplayAliasForRoom(space) || space.roomId } +
    ; + }); + + spaceOptionSection = ( + { + setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space); + }} + value={selectedSpace.roomId} + label={_t("Space selection")} + > + { options } + + ); + } else { + spaceOptionSection =
    + { space.name || getDisplayAliasForRoom(space) || space.roomId } +
    ; + } + + const title = + +
    +

    { _t("Add existing rooms") }

    + { spaceOptionSection } +
    +
    ; + return = ({ matrixClient: cli, space, { - if (checked) { - selectedToAdd.add(room); - } else { - selectedToAdd.delete(room); - } - setSelectedToAdd(new Set(selectedToAdd)); - } : null} + onFinished={onFinished} + footerPrompt={<> +
    { _t("Want to add a new room instead?") }
    + onCreateRoomClick(cli, space)} kind="link"> + { _t("Create a new room") } + + } />
    -
    - { footer } -
    + onFinished(false)} />
    ; }; diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx new file mode 100644 index 0000000000..1ae50dd66f --- /dev/null +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -0,0 +1,106 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {useState} from "react"; + +import QuestionDialog from './QuestionDialog'; +import { _t } from '../../../languageHandler'; +import Field from "../elements/Field"; +import SdkConfig from "../../../SdkConfig"; +import {IDialogProps} from "./IDialogProps"; +import SettingsStore from "../../../settings/SettingsStore"; +import {submitFeedback} from "../../../rageshake/submit-rageshake"; +import StyledCheckbox from "../elements/StyledCheckbox"; +import Modal from "../../../Modal"; +import InfoDialog from "./InfoDialog"; +import AccessibleButton from "../elements/AccessibleButton"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import {Action} from "../../../dispatcher/actions"; +import {USER_LABS_TAB} from "./UserSettingsDialog"; + +interface IProps extends IDialogProps { + featureId: string; +} + +const BetaFeedbackDialog: React.FC = ({featureId, onFinished}) => { + const info = SettingsStore.getBetaInfo(featureId); + + const [comment, setComment] = useState(""); + const [canContact, setCanContact] = useState(false); + + const sendFeedback = async (ok: boolean) => { + if (!ok) return onFinished(false); + + submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact); + onFinished(true); + + Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, { + title: _t("Beta feedback"), + description: _t("Thank you for your feedback, we really appreciate it."), + button: _t("Done"), + hasCloseButton: false, + fixedWidth: false, + }); + }; + + return ( +
    + { _t(info.feedbackSubheading) } +   + { _t("Your platform and username will be noted to help us use your feedback as much as we can.")} + + { + onFinished(false); + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: USER_LABS_TAB, + }); + }}> + { _t("To leave the beta, visit your settings.") } + +
    + + { + setComment(ev.target.value); + }} + autoFocus={true} + /> + + setCanContact((e.target as HTMLInputElement).checked)} + > + { _t("You may contact me if you have any follow up questions") } + + } + button={_t("Send feedback")} + buttonDisabled={!comment} + onFinished={sendFeedback} + />); +}; + +export default BetaFeedbackDialog; diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.tsx similarity index 60% rename from src/components/views/dialogs/CreateRoomDialog.js rename to src/components/views/dialogs/CreateRoomDialog.tsx index e9dc6e2be0..cce6b6c34c 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -1,6 +1,6 @@ /* Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,27 +15,46 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, {ChangeEvent, createRef, KeyboardEvent, SyntheticEvent} from "react"; import {Room} from "matrix-js-sdk/src/models/room"; -import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; -import withValidation from '../elements/Validation'; -import { _t } from '../../../languageHandler'; +import withValidation, {IFieldState} from '../elements/Validation'; +import {_t} from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {Key} from "../../../Keyboard"; -import {privateShouldBeEncrypted} from "../../../createRoom"; +import {IOpts, Preset, privateShouldBeEncrypted, Visibility} from "../../../createRoom"; import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import Field from "../elements/Field"; +import RoomAliasField from "../elements/RoomAliasField"; +import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; +import DialogButtons from "../elements/DialogButtons"; +import BaseDialog from "../dialogs/BaseDialog"; + +interface IProps { + defaultPublic?: boolean; + defaultName?: string; + parentSpace?: Room; + onFinished(proceed: boolean, opts?: IOpts): void; +} + +interface IState { + isPublic: boolean; + isEncrypted: boolean; + name: string; + topic: string; + alias: string; + detailsOpen: boolean; + noFederate: boolean; + nameIsValid: boolean; + canChangeEncryption: boolean; +} @replaceableComponent("views.dialogs.CreateRoomDialog") -export default class CreateRoomDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - defaultPublic: PropTypes.bool, - parentSpace: PropTypes.instanceOf(Room), - }; +export default class CreateRoomDialog extends React.Component { + private nameField = createRef(); + private aliasField = createRef(); constructor(props) { super(props); @@ -44,7 +63,7 @@ export default class CreateRoomDialog extends React.Component { this.state = { isPublic: this.props.defaultPublic || false, isEncrypted: privateShouldBeEncrypted(), - name: "", + name: this.props.defaultName || "", topic: "", alias: "", detailsOpen: false, @@ -54,26 +73,25 @@ export default class CreateRoomDialog extends React.Component { }; MatrixClientPeg.get().doesServerForceEncryptionForPreset("private") - .then(isForced => this.setState({canChangeEncryption: !isForced})); + .then(isForced => this.setState({ canChangeEncryption: !isForced })); } - _roomCreateOptions() { - const opts = {}; - const createOpts = opts.createOpts = {}; + private roomCreateOptions() { + const opts: IOpts = {}; + const createOpts: IOpts["createOpts"] = opts.createOpts = {}; createOpts.name = this.state.name; if (this.state.isPublic) { - createOpts.visibility = "public"; - createOpts.preset = "public_chat"; + createOpts.visibility = Visibility.Public; + createOpts.preset = Preset.PublicChat; opts.guestAccess = false; - const {alias} = this.state; - const localPart = alias.substr(1, alias.indexOf(":") - 1); - createOpts['room_alias_name'] = localPart; + const { alias } = this.state; + createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1); } if (this.state.topic) { createOpts.topic = this.state.topic; } if (this.state.noFederate) { - createOpts.creation_content = {'m.federate': false}; + createOpts.creation_content = { 'm.federate': false }; } if (!this.state.isPublic) { @@ -98,16 +116,14 @@ export default class CreateRoomDialog extends React.Component { } componentDidMount() { - this._detailsRef.addEventListener("toggle", this.onDetailsToggled); // move focus to first field when showing dialog - this._nameFieldRef.focus(); + this.nameField.current.focus(); } componentWillUnmount() { - this._detailsRef.removeEventListener("toggle", this.onDetailsToggled); } - _onKeyDown = event => { + private onKeyDown = (event: KeyboardEvent) => { if (event.key === Key.ENTER) { this.onOk(); event.preventDefault(); @@ -115,26 +131,26 @@ export default class CreateRoomDialog extends React.Component { } }; - onOk = async () => { - const activeElement = document.activeElement; + private onOk = async () => { + const activeElement = document.activeElement as HTMLElement; if (activeElement) { activeElement.blur(); } - await this._nameFieldRef.validate({allowEmpty: false}); - if (this._aliasFieldRef) { - await this._aliasFieldRef.validate({allowEmpty: false}); + await this.nameField.current.validate({allowEmpty: false}); + if (this.aliasField.current) { + await this.aliasField.current.validate({allowEmpty: false}); } // Validation and state updates are async, so we need to wait for them to complete // first. Queue a `setState` callback and wait for it to resolve. - await new Promise(resolve => this.setState({}, resolve)); - if (this.state.nameIsValid && (!this._aliasFieldRef || this._aliasFieldRef.isValid)) { - this.props.onFinished(true, this._roomCreateOptions()); + await new Promise(resolve => this.setState({}, resolve)); + if (this.state.nameIsValid && (!this.aliasField.current || this.aliasField.current.isValid)) { + this.props.onFinished(true, this.roomCreateOptions()); } else { let field; if (!this.state.nameIsValid) { - field = this._nameFieldRef; - } else if (this._aliasFieldRef && !this._aliasFieldRef.isValid) { - field = this._aliasFieldRef; + field = this.nameField.current; + } else if (this.aliasField.current && !this.aliasField.current.isValid) { + field = this.aliasField.current; } if (field) { field.focus(); @@ -143,49 +159,45 @@ export default class CreateRoomDialog extends React.Component { } }; - onCancel = () => { + private onCancel = () => { this.props.onFinished(false); }; - onNameChange = ev => { - this.setState({name: ev.target.value}); + private onNameChange = (ev: ChangeEvent) => { + this.setState({ name: ev.target.value }); }; - onTopicChange = ev => { - this.setState({topic: ev.target.value}); + private onTopicChange = (ev: ChangeEvent) => { + this.setState({ topic: ev.target.value }); }; - onPublicChange = isPublic => { - this.setState({isPublic}); + private onPublicChange = (isPublic: boolean) => { + this.setState({ isPublic }); }; - onEncryptedChange = isEncrypted => { - this.setState({isEncrypted}); + private onEncryptedChange = (isEncrypted: boolean) => { + this.setState({ isEncrypted }); }; - onAliasChange = alias => { - this.setState({alias}); + private onAliasChange = (alias: string) => { + this.setState({ alias }); }; - onDetailsToggled = ev => { - this.setState({detailsOpen: ev.target.open}); + private onDetailsToggled = (ev: SyntheticEvent) => { + this.setState({ detailsOpen: (ev.target as HTMLDetailsElement).open }); }; - onNoFederateChange = noFederate => { - this.setState({noFederate}); + private onNoFederateChange = (noFederate: boolean) => { + this.setState({ noFederate }); }; - collectDetailsRef = ref => { - this._detailsRef = ref; - }; - - onNameValidate = async fieldState => { - const result = await CreateRoomDialog._validateRoomName(fieldState); + private onNameValidate = async (fieldState: IFieldState) => { + const result = await CreateRoomDialog.validateRoomName(fieldState); this.setState({nameIsValid: result.valid}); return result; }; - static _validateRoomName = withValidation({ + private static validateRoomName = withValidation({ rules: [ { key: "required", @@ -196,18 +208,17 @@ export default class CreateRoomDialog extends React.Component { }); render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Field = sdk.getComponent('views.elements.Field'); - const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); - const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); - let aliasField; if (this.state.isPublic) { const domain = MatrixClientPeg.get().getDomain(); aliasField = (
    - this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} /> +
    ); } @@ -270,16 +281,34 @@ export default class CreateRoomDialog extends React.Component { - +
    - this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" /> - - + + + { publicPrivateLabel } { e2eeSection } { aliasField } -
    - { this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') } +
    + + { this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') } + +Copyright 2018-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,14 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useState, useEffect} from 'react'; -import PropTypes from 'prop-types'; +import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react'; import * as sdk from '../../../index'; import SyntaxHighlight from '../elements/SyntaxHighlight'; import { _t } from '../../../languageHandler'; import Field from "../elements/Field"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; import { PHASE_UNSENT, @@ -30,27 +30,33 @@ import { PHASE_DONE, PHASE_STARTED, PHASE_CANCELLED, + VerificationRequest, } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import WidgetStore from "../../../stores/WidgetStore"; -import {UPDATE_EVENT} from "../../../stores/AsyncStore"; -import {SETTINGS} from "../../../settings/Settings"; -import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore"; +import WidgetStore, { IApp } from "../../../stores/WidgetStore"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import { SETTINGS } from "../../../settings/Settings"; +import SettingsStore, { LEVEL_ORDER } from "../../../settings/SettingsStore"; import Modal from "../../../Modal"; import ErrorDialog from "./ErrorDialog"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { SettingLevel } from '../../../settings/SettingLevel'; -class GenericEditor extends React.PureComponent { - // static propTypes = {onBack: PropTypes.func.isRequired}; +interface IGenericEditorProps { + onBack: () => void; +} - constructor(props) { - super(props); - this._onChange = this._onChange.bind(this); - this.onBack = this.onBack.bind(this); - } +interface IGenericEditorState { + message?: string; + [inputId: string]: boolean | string; +} - onBack() { +abstract class GenericEditor< + P extends IGenericEditorProps = IGenericEditorProps, + S extends IGenericEditorState = IGenericEditorState, +> extends React.PureComponent { + protected onBack = () => { if (this.state.message) { this.setState({ message: null }); } else { @@ -58,47 +64,60 @@ class GenericEditor extends React.PureComponent { } } - _onChange(e) { + protected onChange = (e: ChangeEvent) => { + // @ts-ignore: Unsure how to convince TS this is okay when the state + // type can be extended. this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value}); } - _buttons() { + protected abstract send(); + + protected buttons(): React.ReactNode { return
    - { !this.state.message && } + { !this.state.message && }
    ; } - textInput(id, label) { + protected textInput(id: string, label: string): React.ReactNode { return ; } } -export class SendCustomEvent extends GenericEditor { - static getLabel() { return _t('Send Custom Event'); } - - static propTypes = { - onBack: PropTypes.func.isRequired, - room: PropTypes.instanceOf(Room).isRequired, - forceStateEvent: PropTypes.bool, - forceGeneralEvent: PropTypes.bool, - inputs: PropTypes.object, +interface ISendCustomEventProps extends IGenericEditorProps { + room: Room; + forceStateEvent?: boolean; + forceGeneralEvent?: boolean; + inputs?: { + eventType?: string; + stateKey?: string; + evContent?: string; }; +} + +interface ISendCustomEventState extends IGenericEditorState { + isStateEvent: boolean; + eventType: string; + stateKey: string; + evContent: string; +} + +export class SendCustomEvent extends GenericEditor { + static getLabel() { return _t('Send Custom Event'); } static contextType = MatrixClientContext; constructor(props) { super(props); - this._send = this._send.bind(this); const {eventType, stateKey, evContent} = Object.assign({ eventType: '', @@ -115,7 +134,7 @@ export class SendCustomEvent extends GenericEditor { }; } - send(content) { + private doSend(content: object): Promise { const cli = this.context; if (this.state.isStateEvent) { return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey); @@ -124,7 +143,7 @@ export class SendCustomEvent extends GenericEditor { } } - async _send() { + protected send = async () => { if (this.state.eventType === '') { this.setState({ message: _t('You must specify an event type!') }); return; @@ -133,7 +152,7 @@ export class SendCustomEvent extends GenericEditor { let message; try { const content = JSON.parse(this.state.evContent); - await this.send(content); + await this.doSend(content); message = _t('Event sent!'); } catch (e) { message = _t('Failed to send custom event.') + ' (' + e.toString() + ')'; @@ -147,7 +166,7 @@ export class SendCustomEvent extends GenericEditor {
    { this.state.message }
    - { this._buttons() } + { this.buttons() }
    ; } @@ -163,35 +182,51 @@ export class SendCustomEvent extends GenericEditor {
    + autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
    - { !this.state.message && } + { !this.state.message && } { showTglFlip &&
    - -
    }
    ; } } -class SendAccountData extends GenericEditor { - static getLabel() { return _t('Send Account Data'); } - - static propTypes = { - room: PropTypes.instanceOf(Room).isRequired, - isRoomAccountData: PropTypes.bool, - forceMode: PropTypes.bool, - inputs: PropTypes.object, +interface ISendAccountDataProps extends IGenericEditorProps { + room: Room; + isRoomAccountData: boolean; + forceMode: boolean; + inputs?: { + eventType?: string; + evContent?: string; }; +} + +interface ISendAccountDataState extends IGenericEditorState { + isRoomAccountData: boolean; + eventType: string; + evContent: string; +} + +class SendAccountData extends GenericEditor { + static getLabel() { return _t('Send Account Data'); } static contextType = MatrixClientContext; constructor(props) { super(props); - this._send = this._send.bind(this); const {eventType, evContent} = Object.assign({ eventType: '', @@ -206,7 +241,7 @@ class SendAccountData extends GenericEditor { }; } - send(content) { + private doSend(content: object): Promise { const cli = this.context; if (this.state.isRoomAccountData) { return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content); @@ -214,7 +249,7 @@ class SendAccountData extends GenericEditor { return cli.setAccountData(this.state.eventType, content); } - async _send() { + protected send = async () => { if (this.state.eventType === '') { this.setState({ message: _t('You must specify an event type!') }); return; @@ -223,7 +258,7 @@ class SendAccountData extends GenericEditor { let message; try { const content = JSON.parse(this.state.evContent); - await this.send(content); + await this.doSend(content); message = _t('Event sent!'); } catch (e) { message = _t('Failed to send custom event.') + ' (' + e.toString() + ')'; @@ -237,7 +272,7 @@ class SendAccountData extends GenericEditor {
    { this.state.message }
    - { this._buttons() } + { this.buttons() }
    ; } @@ -247,14 +282,23 @@ class SendAccountData extends GenericEditor {
    + autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
    - { !this.state.message && } + { !this.state.message && } { !this.state.message &&
    - -
    }
    ; @@ -264,17 +308,22 @@ class SendAccountData extends GenericEditor { const INITIAL_LOAD_TILES = 20; const LOAD_TILES_STEP_SIZE = 50; -class FilteredList extends React.PureComponent { - static propTypes = { - children: PropTypes.any, - query: PropTypes.string, - onChange: PropTypes.func, - }; +interface IFilteredListProps { + children: React.ReactElement[]; + query: string; + onChange: (value: string) => void; +} - static filterChildren(children, query) { +interface IFilteredListState { + filteredChildren: React.ReactElement[]; + truncateAt: number; +} + +class FilteredList extends React.PureComponent { + static filterChildren(children: React.ReactElement[], query: string): React.ReactElement[] { if (!query) return children; const lcQuery = query.toLowerCase(); - return children.filter((child) => child.key.toLowerCase().includes(lcQuery)); + return children.filter((child) => child.key.toString().toLowerCase().includes(lcQuery)); } constructor(props) { @@ -295,27 +344,27 @@ class FilteredList extends React.PureComponent { }); } - showAll = () => { + private showAll = () => { this.setState({ truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE, }); }; - createOverflowElement = (overflowCount: number, totalCount: number) => { + private createOverflowElement = (overflowCount: number, totalCount: number) => { return ; }; - onQuery = (ev) => { + private onQuery = (ev: ChangeEvent) => { if (this.props.onChange) this.props.onChange(ev.target.value); }; - getChildren = (start: number, end: number) => { + private getChildren = (start: number, end: number): React.ReactElement[] => { return this.state.filteredChildren.slice(start, end); }; - getChildCount = (): number => { + private getChildCount = (): number => { return this.state.filteredChildren.length; }; @@ -336,28 +385,31 @@ class FilteredList extends React.PureComponent { } } -class RoomStateExplorer extends React.PureComponent { - static getLabel() { return _t('Explore Room State'); } +interface IExplorerProps { + room: Room; + onBack: () => void; +} - static propTypes = { - onBack: PropTypes.func.isRequired, - room: PropTypes.instanceOf(Room).isRequired, - }; +interface IRoomStateExplorerState { + eventType?: string; + event?: MatrixEvent; + editing: boolean; + queryEventType: string; + queryStateKey: string; +} + +class RoomStateExplorer extends React.PureComponent { + static getLabel() { return _t('Explore Room State'); } static contextType = MatrixClientContext; - roomStateEvents: Map>; + private roomStateEvents: Map>; constructor(props) { super(props); this.roomStateEvents = this.props.room.currentState.events; - this.onBack = this.onBack.bind(this); - this.editEv = this.editEv.bind(this); - this.onQueryEventType = this.onQueryEventType.bind(this); - this.onQueryStateKey = this.onQueryStateKey.bind(this); - this.state = { eventType: null, event: null, @@ -368,19 +420,19 @@ class RoomStateExplorer extends React.PureComponent { }; } - browseEventType(eventType) { + private browseEventType(eventType: string) { return () => { this.setState({ eventType }); }; } - onViewSourceClick(event) { + private onViewSourceClick(event: MatrixEvent) { return () => { this.setState({ event }); }; } - onBack() { + private onBack = () => { if (this.state.editing) { this.setState({ editing: false }); } else if (this.state.event) { @@ -392,15 +444,15 @@ class RoomStateExplorer extends React.PureComponent { } } - editEv() { + private editEv = () => { this.setState({ editing: true }); } - onQueryEventType(filterEventType) { + private onQueryEventType = (filterEventType: string) => { this.setState({ queryEventType: filterEventType }); } - onQueryStateKey(filterStateKey) { + private onQueryStateKey = (filterStateKey: string) => { this.setState({ queryStateKey: filterStateKey }); } @@ -472,24 +524,22 @@ class RoomStateExplorer extends React.PureComponent { } } -class AccountDataExplorer extends React.PureComponent { - static getLabel() { return _t('Explore Account Data'); } +interface IAccountDataExplorerState { + isRoomAccountData: boolean; + event?: MatrixEvent; + editing: boolean; + queryEventType: string; + [inputId: string]: boolean | string; +} - static propTypes = { - onBack: PropTypes.func.isRequired, - room: PropTypes.instanceOf(Room).isRequired, - }; +class AccountDataExplorer extends React.PureComponent { + static getLabel() { return _t('Explore Account Data'); } static contextType = MatrixClientContext; constructor(props) { super(props); - this.onBack = this.onBack.bind(this); - this.editEv = this.editEv.bind(this); - this._onChange = this._onChange.bind(this); - this.onQueryEventType = this.onQueryEventType.bind(this); - this.state = { isRoomAccountData: false, event: null, @@ -499,20 +549,20 @@ class AccountDataExplorer extends React.PureComponent { }; } - getData() { + private getData(): Record { if (this.state.isRoomAccountData) { return this.props.room.accountData; } return this.context.store.accountData; } - onViewSourceClick(event) { + private onViewSourceClick(event: MatrixEvent) { return () => { this.setState({ event }); }; } - onBack() { + private onBack = () => { if (this.state.editing) { this.setState({ editing: false }); } else if (this.state.event) { @@ -522,15 +572,15 @@ class AccountDataExplorer extends React.PureComponent { } } - _onChange(e) { + private onChange = (e: ChangeEvent) => { this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value}); } - editEv() { + private editEv = () => { this.setState({ editing: true }); } - onQueryEventType(queryEventType) { + private onQueryEventType = (queryEventType: string) => { this.setState({ queryEventType }); } @@ -580,30 +630,39 @@ class AccountDataExplorer extends React.PureComponent {
    - { !this.state.message &&
    - -
    } +
    + +
    ; } } -class ServersInRoomList extends React.PureComponent { +interface IServersInRoomListState { + query: string; +} + +class ServersInRoomList extends React.PureComponent { static getLabel() { return _t('View Servers in Room'); } - static propTypes = { - onBack: PropTypes.func.isRequired, - room: PropTypes.instanceOf(Room).isRequired, - }; - static contextType = MatrixClientContext; + private servers: React.ReactElement[]; + constructor(props) { super(props); const room = this.props.room; - const servers = new Set(); + const servers = new Set(); room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1])); this.servers = Array.from(servers).map(s => + @@ -1091,7 +1179,11 @@ class SettingsExplorer extends React.Component { } } -const Entries = [ +type DevtoolsDialogEntry = React.JSXElementConstructor & { + getLabel: () => string; +}; + +const Entries: DevtoolsDialogEntry[] = [ SendCustomEvent, RoomStateExplorer, SendAccountData, @@ -1102,43 +1194,36 @@ const Entries = [ SettingsExplorer, ]; -@replaceableComponent("views.dialogs.DevtoolsDialog") -export default class DevtoolsDialog extends React.PureComponent { - static propTypes = { - roomId: PropTypes.string.isRequired, - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + roomId: string; + onFinished: (finished: boolean) => void; +} +interface IState { + mode?: DevtoolsDialogEntry; +} + +@replaceableComponent("views.dialogs.DevtoolsDialog") +export default class DevtoolsDialog extends React.PureComponent { constructor(props) { super(props); - this.onBack = this.onBack.bind(this); - this.onCancel = this.onCancel.bind(this); this.state = { mode: null, }; } - componentWillUnmount() { - this._unmounted = true; - } - - _setMode(mode) { + private setMode(mode: DevtoolsDialogEntry) { return () => { this.setState({ mode }); }; } - onBack() { - if (this.prevMode) { - this.setState({ mode: this.prevMode }); - this.prevMode = null; - } else { - this.setState({ mode: null }); - } + private onBack = () => { + this.setState({ mode: null }); } - onCancel() { + private onCancel = () => { this.props.onFinished(false); } @@ -1165,7 +1250,7 @@ export default class DevtoolsDialog extends React.PureComponent {
    { Entries.map((Entry) => { const label = Entry.getLabel(); - const onClick = this._setMode(Entry); + const onClick = this.setMode(Entry); return ; }) }
    diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index ec9c71ccbe..b006205f11 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -47,10 +47,19 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {mediaFromMxc} from "../../../customisations/Media"; import {getAddressType} from "../../../UserAddress"; +import BaseAvatar from '../avatars/BaseAvatar'; +import AccessibleButton from '../elements/AccessibleButton'; +import { compare } from '../../../utils/strings'; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ +interface IRecentUser { + userId: string, + user: RoomMember, + lastActive: number, +} + export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; export const KIND_CALL_TRANSFER = "call_transfer"; @@ -61,43 +70,41 @@ const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is c // This is the interface that is expected by various components in this file. It is a bit // awkward because it also matches the RoomMember class from the js-sdk with some extra support // for 3PIDs/email addresses. -// -// XXX: We should use TypeScript interfaces instead of this weird "abstract" class. -class Member { +abstract class Member { /** * The display name of this Member. For users this should be their profile's display * name or user ID if none set. For 3PIDs this should be the 3PID address (email). */ - get name(): string { throw new Error("Member class not implemented"); } + public abstract get name(): string; /** * The ID of this Member. For users this should be their user ID. For 3PIDs this should * be the 3PID address (email). */ - get userId(): string { throw new Error("Member class not implemented"); } + public abstract get userId(): string; /** * Gets the MXC URL of this Member's avatar. For users this should be their profile's * avatar MXC URL or null if none set. For 3PIDs this should always be null. */ - getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); } + public abstract getMxcAvatarUrl(): string; } class DirectoryMember extends Member { - _userId: string; - _displayName: string; - _avatarUrl: string; + private readonly _userId: string; + private readonly displayName: string; + private readonly avatarUrl: string; constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) { super(); this._userId = userDirResult.user_id; - this._displayName = userDirResult.display_name; - this._avatarUrl = userDirResult.avatar_url; + this.displayName = userDirResult.display_name; + this.avatarUrl = userDirResult.avatar_url; } // These next class members are for the Member interface get name(): string { - return this._displayName || this._userId; + return this.displayName || this._userId; } get userId(): string { @@ -105,32 +112,32 @@ class DirectoryMember extends Member { } getMxcAvatarUrl(): string { - return this._avatarUrl; + return this.avatarUrl; } } class ThreepidMember extends Member { - _id: string; + private readonly id: string; constructor(id: string) { super(); - this._id = id; + this.id = id; } // This is a getter that would be falsey on all other implementations. Until we have // better type support in the react-sdk we can use this trick to determine the kind // of 3PID we're dealing with, if any. get isEmail(): boolean { - return this._id.includes('@'); + return this.id.includes('@'); } // These next class members are for the Member interface get name(): string { - return this._id; + return this.id; } get userId(): string { - return this._id; + return this.id; } getMxcAvatarUrl(): string { @@ -140,11 +147,11 @@ class ThreepidMember extends Member { interface IDMUserTileProps { member: RoomMember; - onRemove: (RoomMember) => any; + onRemove(member: RoomMember): void; } class DMUserTile extends React.PureComponent { - _onRemove = (e) => { + private onRemove = (e) => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); @@ -153,9 +160,6 @@ class DMUserTile extends React.PureComponent { }; render() { - const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - const avatarSize = 20; const avatar = this.props.member.isEmail ? { closeButton = ( {_t('Remove')} { interface IDMRoomTileProps { member: RoomMember; lastActiveTs: number; - onToggle: (RoomMember) => any; + onToggle(member: RoomMember): void; highlightWord: string; isSelected: boolean; } class DMRoomTile extends React.PureComponent { - _onClick = (e) => { + private onClick = (e) => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); @@ -215,7 +219,7 @@ class DMRoomTile extends React.PureComponent { this.props.onToggle(this.props.member); }; - _highlightName(str: string) { + private highlightName(str: string) { if (!this.props.highlightWord) return str; // We convert things to lowercase for index searching, but pull substrings from @@ -252,8 +256,6 @@ class DMRoomTile extends React.PureComponent { } render() { - const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); - let timestamp = null; if (this.props.lastActiveTs) { const humanTs = humanizeTime(this.props.lastActiveTs); @@ -291,13 +293,13 @@ class DMRoomTile extends React.PureComponent { const caption = this.props.member.isEmail ? _t("Invite by email") - : this._highlightName(this.props.member.userId); + : this.highlightName(this.props.member.userId); return ( -
    +
    {stackedAvatar} -
    {this._highlightName(this.props.member.name)}
    +
    {this.highlightName(this.props.member.name)}
    {caption}
    {timestamp} @@ -308,7 +310,7 @@ class DMRoomTile extends React.PureComponent { interface IInviteDialogProps { // Takes an array of user IDs/emails to invite. - onFinished: (toInvite?: string[]) => any; + onFinished: (toInvite?: string[]) => void; // The kind of invite being performed. Assumed to be KIND_DM if // not provided. @@ -349,8 +351,9 @@ export default class InviteDialog extends React.PureComponent(); + private unmounted = false; constructor(props) { super(props); @@ -378,7 +381,7 @@ export default class InviteDialog extends React.PureComponent { this.setState({consultFirst: ev.target.checked}); } - static buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number}[] { + public static buildRecents(excludedTargetIds: Set): IRecentUser[] { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the @@ -467,7 +472,7 @@ export default class InviteDialog extends React.PureComponent): {userId: string, user: RoomMember}[] { + private buildSuggestions(excludedTargetIds: Set): {userId: string, user: RoomMember}[] { const maxConsideredMembers = 200; const joinedRooms = MatrixClientPeg.get().getRooms() .filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers); @@ -574,7 +579,7 @@ export default class InviteDialog extends React.PureComponent { if (a.score === b.score) { if (a.numRooms === b.numRooms) { - return a.member.userId.localeCompare(b.member.userId); + return compare(a.member.userId, b.member.userId); } return b.numRooms - a.numRooms; @@ -585,7 +590,7 @@ export default class InviteDialog extends React.PureComponent ({userId: m.member.userId, user: m.member})); } - _shouldAbortAfterInviteError(result): boolean { + private shouldAbortAfterInviteError(result): boolean { const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error'); if (failedUsers.length > 0) { console.log("Failed to invite users: ", result); @@ -600,7 +605,7 @@ export default class InviteDialog extends React.PureComponent { + private startDm = async () => { this.setState({busy: true}); const client = MatrixClientPeg.get(); - const targets = this._convertFilter(); + const targets = this.convertFilter(); const targetIds = targets.map(t => t.userId); // Check if there is already a DM with these people and reuse it if possible. @@ -694,11 +699,11 @@ export default class InviteDialog extends React.PureComponent { + private inviteUsers = async () => { const startTime = CountlyAnalytics.getTimestamp(); this.setState({busy: true}); - this._convertFilter(); - const targets = this._convertFilter(); + this.convertFilter(); + const targets = this.convertFilter(); const targetIds = targets.map(t => t.userId); const cli = MatrixClientPeg.get(); @@ -715,7 +720,7 @@ export default class InviteDialog extends React.PureComponent { - this._convertFilter(); - const targets = this._convertFilter(); + private transferCall = async () => { + this.convertFilter(); + const targets = this.convertFilter(); const targetIds = targets.map(t => t.userId); if (targetIds.length > 1) { this.setState({ @@ -790,26 +795,26 @@ export default class InviteDialog extends React.PureComponent { + private onKeyDown = (e) => { if (this.state.busy) return; const value = e.target.value.trim(); const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey; if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) { // when the field is empty and the user hits backspace remove the right-most target e.preventDefault(); - this._removeMember(this.state.targets[this.state.targets.length - 1]); + this.removeMember(this.state.targets[this.state.targets.length - 1]); } else if (value && e.key === Key.ENTER && !hasModifiers) { // when the user hits enter with something in their field try to convert it e.preventDefault(); - this._convertFilter(); + this.convertFilter(); } else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) { // when the user hits space and their input looks like an e-mail/MXID then try to convert it e.preventDefault(); - this._convertFilter(); + this.convertFilter(); } }; - _updateSuggestions = async (term) => { + private updateSuggestions = async (term) => { MatrixClientPeg.get().searchUserDirectory({term}).then(async r => { if (term !== this.state.filterText) { // Discard the results - we were probably too slow on the server-side to make @@ -918,30 +923,30 @@ export default class InviteDialog extends React.PureComponent { + private updateFilter = (e) => { const term = e.target.value; this.setState({filterText: term}); // Debounce server lookups to reduce spam. We don't clear the existing server // results because they might still be vaguely accurate, likewise for races which // could happen here. - if (this._debounceTimer) { - clearTimeout(this._debounceTimer); + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); } - this._debounceTimer = setTimeout(() => { - this._updateSuggestions(term); + this.debounceTimer = setTimeout(() => { + this.updateSuggestions(term); }, 150); // 150ms debounce (human reaction time + some) }; - _showMoreRecents = () => { + private showMoreRecents = () => { this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN}); }; - _showMoreSuggestions = () => { + private showMoreSuggestions = () => { this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN}); }; - _toggleMember = (member: Member) => { + private toggleMember = (member: Member) => { if (!this.state.busy) { let filterText = this.state.filterText; const targets = this.state.targets.map(t => t); // cheap clone for mutation @@ -954,13 +959,13 @@ export default class InviteDialog extends React.PureComponent { + private removeMember = (member: Member) => { const targets = this.state.targets.map(t => t); // cheap clone for mutation const idx = targets.indexOf(member); if (idx >= 0) { @@ -968,12 +973,12 @@ export default class InviteDialog extends React.PureComponent { + private onPaste = async (e) => { if (this.state.filterText) { // if the user has already typed something, just let them // paste normally. @@ -1027,6 +1032,7 @@ export default class InviteDialog extends React.PureComponent 0) { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); @@ -1043,17 +1049,17 @@ export default class InviteDialog extends React.PureComponent { + private onClickInputArea = (e) => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); - if (this._editorRef && this._editorRef.current) { - this._editorRef.current.focus(); + if (this.editorRef && this.editorRef.current) { + this.editorRef.current.focus(); } }; - _onUseDefaultIdentityServerClick = (e) => { + private onUseDefaultIdentityServerClick = (e) => { e.preventDefault(); // Update the IS in account data. Actually using it may trigger terms. @@ -1062,21 +1068,21 @@ export default class InviteDialog extends React.PureComponent { + private onManageSettingsClick = (e) => { e.preventDefault(); dis.fire(Action.ViewUserSettings); this.props.onFinished(); }; - _onCommunityInviteClick = (e) => { + private onCommunityInviteClick = (e) => { this.props.onFinished(); showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId()); }; - _renderSection(kind: "recents"|"suggestions") { + private renderSection(kind: "recents"|"suggestions") { let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions; let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown; - const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this); + const showMoreFn = kind === 'recents' ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this); const lastActive = (m) => kind === 'recents' ? m.lastActive : null; let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions"); let sectionSubname = null; @@ -1156,7 +1162,7 @@ export default class InviteDialog extends React.PureComponent t.userId === r.userId)} /> @@ -1171,32 +1177,32 @@ export default class InviteDialog extends React.PureComponent ( - + )); const input = ( ); return ( -
    +
    {targets} {input}
    ); } - _renderIdentityServerWarning() { + private renderIdentityServerWarning() { if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer || !SettingsStore.getValue(UIFeature.IdentityServer) ) { @@ -1214,8 +1220,8 @@ export default class InviteDialog extends React.PureComponent {sub}, - settings: sub => {sub}, + default: sub => {sub}, + settings: sub => {sub}, }, )}
    ); @@ -1225,7 +1231,7 @@ export default class InviteDialog extends React.PureComponentSettings.", {}, { - settings: sub => {sub}, + settings: sub => {sub}, }, )}
    ); @@ -1298,7 +1304,7 @@ export default class InviteDialog extends React.PureComponent{sub} ); }, @@ -1309,7 +1315,7 @@ export default class InviteDialog extends React.PureComponent; } buttonText = _t("Go"); - goButtonFn = this._startDm; + goButtonFn = this.startDm; } else if (this.props.kind === KIND_INVITE) { const room = MatrixClientPeg.get()?.getRoom(this.props.roomId); const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom(); @@ -1348,7 +1354,7 @@ export default class InviteDialog extends React.PureComponent
    ; }; -NetworkDropdown.propTypes = { - onOptionChange: PropTypes.func.isRequired, - protocols: PropTypes.object, -}; - export default NetworkDropdown; diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 3bb264fb3e..c98a7c3156 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -73,7 +73,7 @@ export default class AccessibleTooltipButton extends React.PureComponent :
    ; + /> : null; return ( { icon } { tooltip } + { this.props.children } ); } diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx index 38be8da9a8..00d9d147f1 100644 --- a/src/components/views/elements/EffectsOverlay.tsx +++ b/src/components/views/elements/EffectsOverlay.tsx @@ -17,7 +17,8 @@ import React, { FunctionComponent, useEffect, useRef } from 'react'; import dis from '../../../dispatcher/dispatcher'; import ICanvasEffect from '../../../effects/ICanvasEffect'; -import {CHAT_EFFECTS} from '../../../effects' +import { CHAT_EFFECTS } from '../../../effects' +import UIStore, { UI_EVENTS } from "../../../stores/UIStore"; interface IProps { roomWidth: number; @@ -37,7 +38,7 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { effect = new Effect(options); effectsRef.current[name] = effect; } catch (err) { - console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err); + console.warn(`Unable to load effect module at '../../../effects/${name}.`, err); } } return effect; @@ -45,8 +46,8 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { useEffect(() => { const resize = () => { - if (canvasRef.current) { - canvasRef.current.height = window.innerHeight; + if (canvasRef.current && canvasRef.current?.height !== UIStore.instance.windowHeight) { + canvasRef.current.height = UIStore.instance.windowHeight; } }; const onAction = (payload: { action: string }) => { @@ -58,12 +59,12 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { } const dispatcherRef = dis.register(onAction); const canvas = canvasRef.current; - canvas.height = window.innerHeight; - window.addEventListener('resize', resize, true); + canvas.height = UIStore.instance.windowHeight; + UIStore.instance.on(UI_EVENTS.Resize, resize); return () => { dis.unregister(dispatcherRef); - window.removeEventListener('resize', resize); + UIStore.instance.off(UI_EVENTS.Resize, resize); // eslint-disable-next-line react-hooks/exhaustive-deps const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored for (const effect in currentEffects) { diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 73d5b91511..23858b860d 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -116,7 +116,7 @@ export default class Flair extends React.Component { render() { if (this.state.profiles.length === 0) { - return ; + return null; } const avatars = this.state.profiles.map((profile, index) => { return ; diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 05d487a9eb..df73e1a8cb 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -108,8 +108,6 @@ export default class ImageView extends React.Component { window.addEventListener("resize", this.calculateZoom); // After the image loads for the first time we want to calculate the zoom this.image.current.addEventListener("load", this.calculateZoom); - // Try to precalculate the zoom from width and height props - this.calculateZoom(); } componentWillUnmount() { @@ -122,11 +120,8 @@ export default class ImageView extends React.Component { const image = this.image.current; const imageWrapper = this.imageWrapper.current; - const width = this.props.width || image.naturalWidth; - const height = this.props.height || image.naturalHeight; - - const zoomX = imageWrapper.clientWidth / width; - const zoomY = imageWrapper.clientHeight / height; + const zoomX = imageWrapper.clientWidth / image.naturalWidth; + const zoomY = imageWrapper.clientHeight / image.naturalHeight; // If the image is smaller in both dimensions set its the zoom to 1 to // display it in its original size @@ -212,6 +207,7 @@ export default class ImageView extends React.Component { a.href = this.props.src; a.download = this.props.name; a.target = "_blank"; + a.rel = "noreferrer noopener"; a.click(); }; @@ -447,16 +443,16 @@ export default class ImageView extends React.Component {
    {info}
    - - + + {zoomOutButton} {zoomInButton} { + static defaultProps = { + w: 16, + h: 16, + } + render() { - const w = this.props.w || 16; - const h = this.props.h || 16; - const imgClass = this.props.imgClassName || ""; - - let imageSource; - if (SettingsStore.getValue('feature_new_spinner')) { - imageSource = require("../../../../res/img/spinner.svg"); - } else { - imageSource = require("../../../../res/img/spinner.gif"); - } - return (
    - + > + {this.props.children} +
    ); } diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index b8734c5afb..9420061a74 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -58,13 +58,8 @@ export default class LanguageDropdown extends React.Component { // If no value is given, we start with the first // country selected, but our parent component // doesn't know this, therefore we do this. - const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); - if (language) { - this.props.onOptionChange(language); - } else { - const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); - this.props.onOptionChange(language); - } + const language = languageHandler.getUserLanguage(); + this.props.onOptionChange(language); } } diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index b2609027d4..32ef0d4da2 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -19,6 +19,7 @@ import {EventType} from 'matrix-js-sdk/src/@types/event'; import classNames from 'classnames'; import AccessibleButton from "./AccessibleButton"; +import Spinner from "./Spinner"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {useTimeout} from "../../../hooks/useTimeout"; import Analytics from "../../../Analytics"; @@ -88,6 +89,12 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva > { children } +
    + { busy ? + : +
    } +
    +
    ; + return null; } return 0) { + loadedEv = await this.getNextEvent(events[0]); + } + this.setState({ - loadedEv: null, + loadedEv, events, - }, this.loadNextEvent); + }); dis.fire(Action.FocusComposer); } diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx index a9eb04d4ec..a531abdf83 100644 --- a/src/components/views/elements/SSOButtons.tsx +++ b/src/components/views/elements/SSOButtons.tsx @@ -112,7 +112,7 @@ interface IProps { const MAX_PER_ROW = 6; const SSOButtons: React.FC = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => { - const providers = flow["org.matrix.msc2858.identity_providers"] || []; + const providers = flow.identity_providers || []; if (providers.length < 2) { return
    { - let imageSource; - if (SettingsStore.getValue('feature_new_spinner')) { - imageSource = require("../../../../res/img/spinner.svg"); - } else { - imageSource = require("../../../../res/img/spinner.gif"); - } +const Spinner = ({w = 32, h = 32, message}) => ( +
    + { message &&
    { message }
     
    } +
    +
    +); - return ( -
    - { message &&
    { message}
     
    } - -
    - ); -}; Spinner.propTypes = { w: PropTypes.number, h: PropTypes.number, - imgClassName: PropTypes.string, message: PropTypes.node, }; diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index 062d26c852..0202c6b02f 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -22,6 +22,7 @@ import React, {Component, CSSProperties} from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import UIStore from "../../../stores/UIStore"; const MIN_TOOLTIP_HEIGHT = 25; @@ -69,7 +70,10 @@ export default class Tooltip extends React.Component { this.tooltipContainer = document.createElement("div"); this.tooltipContainer.className = "mx_Tooltip_wrapper"; document.body.appendChild(this.tooltipContainer); - window.addEventListener('scroll', this.renderTooltip, true); + window.addEventListener('scroll', this.renderTooltip, { + passive: true, + capture: true, + }); this.parent = ReactDOM.findDOMNode(this).parentNode as Element; @@ -84,7 +88,9 @@ export default class Tooltip extends React.Component { public componentWillUnmount() { ReactDOM.unmountComponentAtNode(this.tooltipContainer); document.body.removeChild(this.tooltipContainer); - window.removeEventListener('scroll', this.renderTooltip, true); + window.removeEventListener('scroll', this.renderTooltip, { + capture: true, + }); } private updatePosition(style: CSSProperties) { @@ -97,15 +103,15 @@ export default class Tooltip extends React.Component { // we need so that we're still centered. offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT); } - + const width = UIStore.instance.windowWidth; const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset; const top = baseTop + offset; - const right = window.innerWidth - parentBox.right - window.pageXOffset - 16; + const right = width - parentBox.right - window.pageXOffset - 16; const left = parentBox.right + window.pageXOffset + 6; const horizontalCenter = parentBox.right - window.pageXOffset - (parentBox.width / 2); switch (this.props.alignment) { case Alignment.Natural: - if (parentBox.right > window.innerWidth / 2) { + if (parentBox.right > width / 2) { style.right = right; style.top = top; break; diff --git a/src/components/views/elements/TooltipButton.js b/src/components/views/elements/TooltipButton.tsx similarity index 80% rename from src/components/views/elements/TooltipButton.js rename to src/components/views/elements/TooltipButton.tsx index c5ebb3b1aa..191018cc19 100644 --- a/src/components/views/elements/TooltipButton.js +++ b/src/components/views/elements/TooltipButton.tsx @@ -19,19 +19,30 @@ import React from 'react'; import * as sdk from '../../../index'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.elements.TooltipButton") -export default class TooltipButton extends React.Component { - state = { - hover: false, - }; +interface IProps { + helpText: string; +} - onMouseOver = () => { +interface IState { + hover: boolean; +} + +@replaceableComponent("views.elements.TooltipButton") +export default class TooltipButton extends React.Component { + constructor(props) { + super(props); + this.state = { + hover: false, + }; + } + + private onMouseOver = () => { this.setState({ hover: true, }); }; - onMouseLeave = () => { + private onMouseLeave = () => { this.setState({ hover: false, }); diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx index 4a2a83465d..d65de7697a 100644 --- a/src/components/views/messages/MVoiceMessageBody.tsx +++ b/src/components/views/messages/MVoiceMessageBody.tsx @@ -71,10 +71,14 @@ export default class MVoiceMessageBody extends React.PureComponent { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -122,6 +123,10 @@ export default class MessageActionBar extends React.PureComponent { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { this.props.mxEvent.on("Event.status", this.onSent); } + + const client = MatrixClientPeg.get(); + client.decryptEventIfNeeded(this.props.mxEvent); + if (this.props.mxEvent.isBeingDecrypted()) { this.props.mxEvent.once("Event.decrypted", this.onDecrypted); } diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.tsx similarity index 50% rename from src/components/views/messages/ReactionsRow.js rename to src/components/views/messages/ReactionsRow.tsx index d5c8ea2ac9..0180baa466 100644 --- a/src/components/views/messages/ReactionsRow.js +++ b/src/components/views/messages/ReactionsRow.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,35 +14,72 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React from "react"; +import classNames from "classnames"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Relations } from "matrix-js-sdk/src/models/relations"; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { isContentActionable } from '../../../utils/EventUtils'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import { aboveLeftOf, ContextMenu, useContextMenu } from "../../structures/ContextMenu"; +import ReactionPicker from "../emojipicker/ReactionPicker"; +import ReactionsRowButton from "./ReactionsRowButton"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; // The maximum number of reactions to initially show on a message. const MAX_ITEMS_WHEN_LIMITED = 8; -@replaceableComponent("views.messages.ReactionsRow") -export default class ReactionsRow extends React.PureComponent { - static propTypes = { - // The event we're displaying reactions for - mxEvent: PropTypes.object.isRequired, - // The Relations model from the JS SDK for reactions to `mxEvent` - reactions: PropTypes.object, +const ReactButton = ({ mxEvent, reactions }: IProps) => { + const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + + let contextMenu; + if (menuDisplayed) { + const buttonRect = button.current.getBoundingClientRect(); + contextMenu = + + ; } - constructor(props) { - super(props); + return + { + e.preventDefault(); + openMenu(); + }} + isExpanded={menuDisplayed} + inputRef={button} + /> - if (props.reactions) { - props.reactions.on("Relations.add", this.onReactionsChange); - props.reactions.on("Relations.remove", this.onReactionsChange); - props.reactions.on("Relations.redaction", this.onReactionsChange); - } + { contextMenu } + ; +}; + +interface IProps { + // The event we're displaying reactions for + mxEvent: MatrixEvent; + // The Relations model from the JS SDK for reactions to `mxEvent` + reactions?: Relations; +} + +interface IState { + myReactions: MatrixEvent[]; + showAll: boolean; +} + +@replaceableComponent("views.messages.ReactionsRow") +export default class ReactionsRow extends React.PureComponent { + static contextType = MatrixClientContext; + + constructor(props, context) { + super(props, context); this.state = { myReactions: this.getMyReactions(), @@ -50,7 +87,33 @@ export default class ReactionsRow extends React.PureComponent { }; } - componentDidUpdate(prevProps) { + componentDidMount() { + const { mxEvent, reactions } = this.props; + + if (mxEvent.isBeingDecrypted() || mxEvent.shouldAttemptDecryption()) { + mxEvent.once("Event.decrypted", this.onDecrypted); + } + + if (reactions) { + reactions.on("Relations.add", this.onReactionsChange); + reactions.on("Relations.remove", this.onReactionsChange); + reactions.on("Relations.redaction", this.onReactionsChange); + } + } + + componentWillUnmount() { + const { mxEvent, reactions } = this.props; + + mxEvent.off("Event.decrypted", this.onDecrypted); + + if (reactions) { + reactions.off("Relations.add", this.onReactionsChange); + reactions.off("Relations.remove", this.onReactionsChange); + reactions.off("Relations.redaction", this.onReactionsChange); + } + } + + componentDidUpdate(prevProps: IProps) { if (prevProps.reactions !== this.props.reactions) { this.props.reactions.on("Relations.add", this.onReactionsChange); this.props.reactions.on("Relations.remove", this.onReactionsChange); @@ -59,24 +122,12 @@ export default class ReactionsRow extends React.PureComponent { } } - componentWillUnmount() { - if (this.props.reactions) { - this.props.reactions.removeListener( - "Relations.add", - this.onReactionsChange, - ); - this.props.reactions.removeListener( - "Relations.remove", - this.onReactionsChange, - ); - this.props.reactions.removeListener( - "Relations.redaction", - this.onReactionsChange, - ); - } + private onDecrypted = () => { + // Decryption changes whether the event is actionable + this.forceUpdate(); } - onReactionsChange = () => { + private onReactionsChange = () => { // TODO: Call `onHeightChanged` as needed this.setState({ myReactions: this.getMyReactions(), @@ -87,12 +138,12 @@ export default class ReactionsRow extends React.PureComponent { this.forceUpdate(); } - getMyReactions() { + private getMyReactions() { const reactions = this.props.reactions; if (!reactions) { return null; } - const userId = MatrixClientPeg.get().getUserId(); + const userId = this.context.getUserId(); const myReactions = reactions.getAnnotationsBySender()[userId]; if (!myReactions) { return null; @@ -100,7 +151,7 @@ export default class ReactionsRow extends React.PureComponent { return [...myReactions.values()]; } - onShowAllClick = () => { + private onShowAllClick = () => { this.setState({ showAll: true, }); @@ -114,7 +165,6 @@ export default class ReactionsRow extends React.PureComponent { return null; } - const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton'); let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => { const count = events.size; if (!count) { @@ -136,6 +186,8 @@ export default class ReactionsRow extends React.PureComponent { />; }).filter(item => !!item); + if (!items.length) return null; + // Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items. // The "+ 1" ensure that the "show all" reveals something that takes up // more space than the button itself. @@ -151,13 +203,22 @@ export default class ReactionsRow extends React.PureComponent { ; } + const cli = this.context; + + let addReactionButton; + const room = cli.getRoom(mxEvent.getRoomId()); + if (room.getMyMembership() === "join" && room.currentState.maySendEvent(EventType.Reaction, cli.getUserId())) { + addReactionButton = ; + } + return
    - {items} - {showAllButton} + { items } + { showAllButton } + { addReactionButton }
    ; } } diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.tsx similarity index 73% rename from src/components/views/messages/ReactionsRowButton.js rename to src/components/views/messages/ReactionsRowButton.tsx index b37a949e57..d163f1ad30 100644 --- a/src/components/views/messages/ReactionsRowButton.js +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,49 +14,54 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; +import React from "react"; +import classNames from "classnames"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import dis from "../../../dispatcher/dispatcher"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip"; +import AccessibleButton from "../elements/AccessibleButton"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; + +interface IProps { + // The event we're displaying reactions for + mxEvent: MatrixEvent; + // The reaction content / key / emoji + content: string; + // The count of votes for this key + count: number; + // A Set of Matrix reaction events for this key + reactionEvents: Set; + // A possible Matrix event if the current user has voted for this type + myReactionEvent?: MatrixEvent; +} + +interface IState { + tooltipRendered: boolean; + tooltipVisible: boolean; +} @replaceableComponent("views.messages.ReactionsRowButton") -export default class ReactionsRowButton extends React.PureComponent { - static propTypes = { - // The event we're displaying reactions for - mxEvent: PropTypes.object.isRequired, - // The reaction content / key / emoji - content: PropTypes.string.isRequired, - // The count of votes for this key - count: PropTypes.number.isRequired, - // A Set of Martix reaction events for this key - reactionEvents: PropTypes.object.isRequired, - // A possible Matrix event if the current user has voted for this type - myReactionEvent: PropTypes.object, - } +export default class ReactionsRowButton extends React.PureComponent { + static contextType = MatrixClientContext; - constructor(props) { - super(props); + state = { + tooltipRendered: false, + tooltipVisible: false, + }; - this.state = { - tooltipVisible: false, - }; - } - - onClick = (ev) => { + onClick = () => { const { mxEvent, myReactionEvent, content } = this.props; if (myReactionEvent) { - MatrixClientPeg.get().redactEvent( + this.context.redactEvent( mxEvent.getRoomId(), myReactionEvent.getId(), ); } else { - MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", { + this.context.sendEvent(mxEvent.getRoomId(), "m.reaction", { "m.relates_to": { "rel_type": "m.annotation", "event_id": mxEvent.getId(), @@ -83,8 +88,6 @@ export default class ReactionsRowButton extends React.PureComponent { } render() { - const ReactionsRowButtonTooltip = - sdk.getComponent('messages.ReactionsRowButtonTooltip'); const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props; const classes = classNames({ @@ -102,7 +105,7 @@ export default class ReactionsRowButton extends React.PureComponent { />; } - const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); + const room = this.context.getRoom(mxEvent.getRoomId()); let label; if (room) { const senders = []; @@ -130,7 +133,6 @@ export default class ReactionsRowButton extends React.PureComponent { ); } const isPeeking = room.getMyMembership() !== "join"; - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return ; + visible: boolean; +} @replaceableComponent("views.messages.ReactionsRowButtonTooltip") -export default class ReactionsRowButtonTooltip extends React.PureComponent { - static propTypes = { - // The event we're displaying reactions for - mxEvent: PropTypes.object.isRequired, - // The reaction content / key / emoji - content: PropTypes.string.isRequired, - // A Set of Martix reaction events for this key - reactionEvents: PropTypes.object.isRequired, - visible: PropTypes.bool.isRequired, - } +export default class ReactionsRowButtonTooltip extends React.PureComponent { + static contextType = MatrixClientContext; render() { - const Tooltip = sdk.getComponent('elements.Tooltip'); const { content, reactionEvents, mxEvent, visible } = this.props; - const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); + const room = this.context.getRoom(mxEvent.getRoomId()); let tooltipLabel; if (room) { const senders = []; diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index bd10526799..8f10954370 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -31,21 +31,23 @@ export default class SenderProfile extends React.Component { static contextType = MatrixClientContext; - state = { - userGroups: null, - relatedGroups: [], - }; + constructor(props) { + super(props); + const senderId = this.props.mxEvent.getSender(); + this.state = { + userGroups: FlairStore.cachedPublicisedGroups(senderId) || [], + relatedGroups: [], + }; + } componentDidMount() { this.unmounted = false; this._updateRelatedGroups(); - FlairStore.getPublicisedGroupsCached( - this.context, this.props.mxEvent.getSender(), - ).then((userGroups) => { - if (this.unmounted) return; - this.setState({userGroups}); - }); + if (this.state.userGroups.length === 0) { + this.getPublicisedGroups(); + } + this.context.on('RoomState.events', this.onRoomStateEvents); } @@ -55,6 +57,15 @@ export default class SenderProfile extends React.Component { this.context.removeListener('RoomState.events', this.onRoomStateEvents); } + async getPublicisedGroups() { + if (!this.unmounted) { + const userGroups = await FlairStore.getPublicisedGroupsCached( + this.context, this.props.mxEvent.getSender(), + ); + this.setState({userGroups}); + } + } + onRoomStateEvents = event => { if (event.getType() === 'm.room.related_groups' && event.getRoomId() === this.props.mxEvent.getRoomId() @@ -93,10 +104,10 @@ export default class SenderProfile extends React.Component { const {msgtype} = mxEvent.getContent(); if (msgtype === 'm.emote') { - return ; // emote message must include the name so don't duplicate it + return null; // emote message must include the name so don't duplicate it } - let flair =
    ; + let flair = null; if (this.props.enableFlair) { const displayedGroups = this._getDisplayedGroups( this.state.userGroups, this.state.relatedGroups, @@ -110,19 +121,12 @@ export default class SenderProfile extends React.Component { const nameElem = name || ''; - // Name + flair - const nameFlair = - - { nameElem } - - { flair } - ; - return ( -
    -
    - { nameFlair } -
    +
    + + { nameElem } + + { flair }
    ); } diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index b963e741a1..3adfea6ee6 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -36,6 +36,7 @@ import {toRightOf} from "../../structures/ContextMenu"; import {copyPlaintext} from "../../../utils/strings"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import UIStore from "../../../stores/UIStore"; @replaceableComponent("views.messages.TextualBody") export default class TextualBody extends React.Component { @@ -143,7 +144,7 @@ export default class TextualBody extends React.Component { _addCodeExpansionButton(div, pre) { // Calculate how many percent does the pre element take up. // If it's less than 30% we don't add the expansion button. - const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100; + const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100; if (percentageOfViewport < 30) return; const button = document.createElement("span"); @@ -277,15 +278,15 @@ export default class TextualBody extends React.Component { // pass only the first child which is the event tile otherwise this recurses on edited events let links = this.findLinks([this._content.current]); if (links.length) { - // de-dup the links (but preserve ordering) - const seen = new Set(); - links = links.filter((link) => { - if (seen.has(link)) return false; - seen.add(link); - return true; - }); + // de-duplicate the links after stripping hashes as they don't affect the preview + // using a set here maintains the order + links = Array.from(new Set(links.map(link => { + const url = new URL(link); + url.hash = ""; + return url.toString(); + }))); - this.setState({ links: links }); + this.setState({ links }); // lazy-load the hidden state of the preview widget from localstorage if (global.localStorage) { diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js index adc7a248cd..2ec567c5ad 100644 --- a/src/components/views/messages/ViewSourceEvent.js +++ b/src/components/views/messages/ViewSourceEvent.js @@ -18,6 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; @replaceableComponent("views.messages.ViewSourceEvent") export default class ViewSourceEvent extends React.PureComponent { @@ -36,6 +37,10 @@ export default class ViewSourceEvent extends React.PureComponent { componentDidMount() { const {mxEvent} = this.props; + + const client = MatrixClientPeg.get(); + client.decryptEventIfNeeded(mxEvent); + if (mxEvent.isBeingDecrypted()) { mxEvent.once("Event.decrypted", () => this.forceUpdate()); } diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx index f006975b08..3c93cf6470 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.tsx +++ b/src/components/views/right_panel/GroupHeaderButtons.tsx @@ -21,12 +21,12 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import HeaderButtons, {HeaderKind} from './HeaderButtons'; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from "../../../dispatcher/actions"; -import {ActionPayload} from "../../../dispatcher/payloads"; -import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import HeaderButtons, { HeaderKind } from './HeaderButtons'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; +import { ActionPayload } from "../../../dispatcher/payloads"; +import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; const GROUP_PHASES = [ RightPanelPhases.GroupMemberInfo, @@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons { }; renderButtons() { - return [ - + , - + , - ]; + /> + ; } } diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx index 2bc360e380..cdf4f44e06 100644 --- a/src/components/views/right_panel/HeaderButton.tsx +++ b/src/components/views/right_panel/HeaderButton.tsx @@ -22,15 +22,13 @@ import React from 'react'; import classNames from 'classnames'; import Analytics from '../../../Analytics'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // Whether this button is highlighted isHighlighted: boolean; // click handler onClick: () => void; - // The badge to display above the icon - badge?: React.ReactNode; // The parameters to track the click event analytics: Parameters; @@ -40,31 +38,29 @@ interface IProps { title: string; } -// TODO: replace this, the composer buttons and the right panel buttons with a unified -// representation +// TODO: replace this, the composer buttons and the right panel buttons with a unified representation @replaceableComponent("views.right_panel.HeaderButton") export default class HeaderButton extends React.Component { - constructor(props: IProps) { - super(props); - this.onClick = this.onClick.bind(this); - } - - private onClick() { + private onClick = () => { Analytics.trackEvent(...this.props.analytics); this.props.onClick(); - } + }; public render() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {isHighlighted, onClick, analytics, name, title, ...props} = this.props; + const classes = classNames({ mx_RightPanel_headerButton: true, - mx_RightPanel_headerButton_highlight: this.props.isHighlighted, - [`mx_RightPanel_${this.props.name}`]: true, + mx_RightPanel_headerButton_highlight: isHighlighted, + [`mx_RightPanel_${name}`]: true, }); return ; diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 2144292679..6d44a081d9 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -21,14 +21,14 @@ limitations under the License. import React from 'react'; import dis from '../../../dispatcher/dispatcher'; import RightPanelStore from "../../../stores/RightPanelStore"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from '../../../dispatcher/actions'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from '../../../dispatcher/actions'; import { SetRightPanelPhasePayload, SetRightPanelPhaseRefireParams, } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; -import {EventSubscription} from "fbemitter"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import type { EventSubscription } from "fbemitter"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; export enum HeaderKind { Room = "room", @@ -43,11 +43,11 @@ interface IState { interface IProps {} @replaceableComponent("views.right_panel.HeaderButtons") -export default abstract class HeaderButtons extends React.Component { +export default abstract class HeaderButtons

    extends React.Component { private storeToken: EventSubscription; private dispatcherRef: string; - constructor(props: IProps, kind: HeaderKind) { + constructor(props: IProps & P, kind: HeaderKind) { super(props); const rps = RightPanelStore.getSharedInstance(); @@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx new file mode 100644 index 0000000000..a3f1f2d9df --- /dev/null +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -0,0 +1,176 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {useCallback, useContext, useEffect, useState} from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomState } from "matrix-js-sdk/src/models/room-state"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventType } from 'matrix-js-sdk/src/@types/event'; + +import { _t } from "../../../languageHandler"; +import BaseCard from "./BaseCard"; +import Spinner from "../elements/Spinner"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import PinningUtils from "../../../utils/PinningUtils"; +import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; +import PinnedEventTile from "../rooms/PinnedEventTile"; + +interface IProps { + room: Room; + onClose(): void; +} + +export const usePinnedEvents = (room: Room): string[] => { + const [pinnedEvents, setPinnedEvents] = useState([]); + + const update = useCallback((ev?: MatrixEvent) => { + if (!room) return; + if (ev && ev.getType() !== EventType.RoomPinnedEvents) return; + setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []); + }, [room]); + + useEventEmitter(room?.currentState, "RoomState.events", update); + useEffect(() => { + update(); + return () => { + setPinnedEvents([]); + }; + }, [update]); + return pinnedEvents; +}; + +export const ReadPinsEventId = "im.vector.room.read_pins"; + +export const useReadPinnedEvents = (room: Room): Set => { + const [readPinnedEvents, setReadPinnedEvents] = useState>(new Set()); + + const update = useCallback((ev?: MatrixEvent) => { + if (!room) return; + if (ev && ev.getType() !== ReadPinsEventId) return; + const readPins = room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids; + setReadPinnedEvents(new Set(readPins || [])); + }, [room]); + + useEventEmitter(room, "Room.accountData", update); + useEffect(() => { + update(); + return () => { + setReadPinnedEvents(new Set()); + }; + }, [update]); + return readPinnedEvents; +}; + +const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => { + const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); + + const update = useCallback(() => { + if (!room) return; + setValue(mapper(room.currentState)); + }, [room, mapper]); + + useEventEmitter(room?.currentState, "RoomState.events", update); + useEffect(() => { + update(); + return () => { + setValue(undefined); + }; + }, [update]); + return value; +}; + +const PinnedMessagesCard = ({ room, onClose }: IProps) => { + const cli = useContext(MatrixClientContext); + const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli)); + const pinnedEventIds = usePinnedEvents(room); + const readPinnedEvents = useReadPinnedEvents(room); + + useEffect(() => { + const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id)); + if (newlyRead.length > 0) { + // clear out any read pinned events which no longer are pinned + cli.setRoomAccountData(room.roomId, ReadPinsEventId, { + event_ids: pinnedEventIds, + }); + } + }, [cli, room.roomId, pinnedEventIds, readPinnedEvents]); + + const pinnedEvents = useAsyncMemo(() => { + const promises = pinnedEventIds.map(async eventId => { + const timelineSet = room.getUnfilteredTimelineSet(); + const localEvent = timelineSet?.getTimelineForEvent(eventId)?.getEvents().find(e => e.getId() === eventId); + if (localEvent) return localEvent; + + try { + const evJson = await cli.fetchRoomEvent(room.roomId, eventId); + const event = new MatrixEvent(evJson); + if (event.isEncrypted()) { + await cli.decryptEventIfNeeded(event); // TODO await? + } + if (event && PinningUtils.isPinnable(event)) { + return event; + } + } catch (err) { + console.error("Error looking up pinned event " + eventId + " in room " + room.roomId); + console.error(err); + } + return null; + }); + + return Promise.all(promises); + }, [cli, room, pinnedEventIds], null); + + let content; + if (!pinnedEvents) { + content = ; + } else if (pinnedEvents.length > 0) { + let onUnpinClicked; + if (canUnpin) { + onUnpinClicked = async (event: MatrixEvent) => { + const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ""); + if (pinnedEvents?.getContent()?.pinned) { + const pinned = pinnedEvents.getContent().pinned; + const index = pinned.indexOf(event.getId()); + if (index !== -1) { + pinned.splice(index, 1); + await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, ""); + } + } + }; + } + + // show them in reverse, with latest pinned at the top + content = pinnedEvents.filter(Boolean).reverse().map(ev => ( + + )); + } else { + content =

    +

    {_t("You’re all caught up")}

    +

    {_t("You have no visible notifications.")}

    +
    ; + } + + return { _t("Pinned messages") }} + className="mx_PinnedMessagesCard" + onClose={onClose} + > + { content } + ; +}; + +export default PinnedMessagesCard; diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 0571622e64..54e18e4529 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -18,15 +18,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import {_t} from '../../../languageHandler'; +import React from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import HeaderButtons, {HeaderKind} from './HeaderButtons'; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from "../../../dispatcher/actions"; -import {ActionPayload} from "../../../dispatcher/payloads"; +import HeaderButtons, { HeaderKind } from './HeaderButtons'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; +import { ActionPayload } from "../../../dispatcher/payloads"; import RightPanelStore from "../../../stores/RightPanelStore"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { useSettingValue } from "../../../hooks/useSettings"; +import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard'; const ROOM_INFO_PHASES = [ RightPanelPhases.RoomSummary, @@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [ RightPanelPhases.Room3pidMemberInfo, ]; +const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => { + const pinningEnabled = useSettingValue("feature_pinning"); + const pinnedEvents = usePinnedEvents(pinningEnabled && room); + const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room); + if (!pinningEnabled) return null; + + let unreadIndicator; + if (pinnedEvents.some(id => !readPinnedEvents.has(id))) { + unreadIndicator =
    ; + } + + return + { unreadIndicator } + ; +}; + +interface IProps { + room?: Room; +} + @replaceableComponent("views.right_panel.RoomHeaderButtons") -export default class RoomHeaderButtons extends HeaderButtons { - constructor(props) { +export default class RoomHeaderButtons extends HeaderButtons { + constructor(props: IProps) { super(props, HeaderKind.Room); } @@ -80,24 +110,32 @@ export default class RoomHeaderButtons extends HeaderButtons { this.setPhase(RightPanelPhases.NotificationPanel); }; + private onPinnedMessagesClicked = () => { + // This toggles for us, if needed + this.setPhase(RightPanelPhases.PinnedMessages); + }; + public renderButtons() { - return [ + return <> + , + /> , - ]; + /> + ; } } diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index bbe61807ae..937037f644 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -45,6 +45,8 @@ import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../struc import WidgetContextMenu from "../context_menus/WidgetContextMenu"; import {useRoomMemberCount} from "../../../hooks/useRoomMembers"; import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; +import RoomName from "../elements/RoomName"; +import UIStore from "../../../stores/UIStore"; interface IProps { room: Room; @@ -115,8 +117,8 @@ const AppRow: React.FC = ({ app, room }) => { const rect = handle.current.getBoundingClientRect(); contextMenu = ; @@ -249,7 +251,13 @@ const RoomSummaryCard: React.FC = ({ room, onClose }) => { />
    -

    { room.name }

    + + { name => ( +

    + { name } +

    + )} +
    { alias }
    diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 2fa5fd89d3..d6c97f9cf2 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -17,18 +17,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; -import {MatrixClient} from 'matrix-js-sdk/src/client'; -import {RoomMember} from 'matrix-js-sdk/src/models/room-member'; -import {User} from 'matrix-js-sdk/src/models/user'; -import {Room} from 'matrix-js-sdk/src/models/room'; -import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; -import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; +import { User } from 'matrix-js-sdk/src/models/user'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import dis from '../../../dispatcher/dispatcher'; import Modal from '../../../Modal'; -import {_t} from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import AccessibleButton from '../elements/AccessibleButton'; @@ -39,18 +40,18 @@ import MultiInviter from "../../../utils/MultiInviter"; import GroupStore from "../../../stores/GroupStore"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import {textualPowerLevel} from '../../../Roles'; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import { textualPowerLevel } from '../../../Roles'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import EncryptionPanel from "./EncryptionPanel"; -import {useAsyncMemo} from '../../../hooks/useAsyncMemo'; -import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification'; -import {Action} from "../../../dispatcher/actions"; +import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; +import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; +import { Action } from "../../../dispatcher/actions"; import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog"; -import {useIsEncrypted} from "../../../hooks/useIsEncrypted"; +import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; -import {E2EStatus} from "../../../utils/ShieldUtils"; +import { E2EStatus } from "../../../utils/ShieldUtils"; import ImageView from "../elements/ImageView"; import Spinner from "../elements/Spinner"; import PowerSelector from "../elements/PowerSelector"; @@ -65,9 +66,10 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { mediaFromMxc } from "../../../customisations/Media"; +import UIStore from "../../../stores/UIStore"; -interface IDevice { +export interface IDevice { deviceId: string; ambiguous?: boolean; getDisplayName(): string; @@ -187,9 +189,15 @@ function DeviceItem({userId, device}: {userId: string, device: IDevice}) { verifyDevice(cli.getUser(userId), device); }; - const deviceName = device.ambiguous ? - (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : - device.getDisplayName(); + let deviceName; + if (!device.getDisplayName()?.trim()) { + deviceName = device.deviceId; + } else { + deviceName = device.ambiguous ? + device.getDisplayName() + " (" + device.deviceId + ")" : + device.getDisplayName(); + } + let trustedLabel = null; if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); @@ -507,9 +515,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => { } else { setPowerLevels({}); } - return () => { - setPowerLevels({}); - }; }, [room]); useEventEmitter(cli, "RoomState.events", update); @@ -1301,7 +1306,7 @@ const BasicUserInfo: React.FC<{ } if (pendingUpdateCount > 0) { - spinner = ; + spinner = ; } let memberDetails; @@ -1442,8 +1447,8 @@ const UserInfoHeader: React.FC<{ ; } -interface IPropsWithEncryptionPanel extends React.ComponentProps { - user: Member; - groupId: void; - room: Room; - phase: RightPanelPhases.EncryptionPanel; - onClose(): void; -} - -type Props = IProps | IPropsWithEncryptionPanel; - -const UserInfo: React.FC = ({ +const UserInfo: React.FC = ({ user, groupId, room, diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index 56e522e206..d7493e0512 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -30,6 +30,7 @@ import { Action } from "../../../dispatcher/actions"; import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu"; import WidgetContextMenu from "../context_menus/WidgetContextMenu"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; +import UIStore from "../../../stores/UIStore"; interface IProps { room: Room; @@ -65,7 +66,7 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { contextMenu = ( `mx_Autocomplete_Completion_${number}`; @@ -136,7 +137,7 @@ export default class Autocomplete extends React.PureComponent { processQuery(query: string, selection: ISelectionRange) { return this.autocompleter.getCompletions( - query, selection, this.state.forceComplete, + query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES, ).then((completions) => { // Only ever process the completions for the most recent query being processed if (query !== this.queryRequested) { diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index b006fe8c8d..f0980af7ae 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; import * as sdk from '../../../index'; -import {_t} from '../../../languageHandler'; +import {_t, _td} from '../../../languageHandler'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; @@ -24,16 +24,19 @@ import {getCaretOffsetAndText} from '../../../editor/dom'; import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; import {findEditableEvent} from '../../../utils/EventUtils'; import {parseEvent} from '../../../editor/deserialize'; -import {PartCreator} from '../../../editor/parts'; +import {CommandPartCreator} from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import classNames from 'classnames'; import {EventStatus} from 'matrix-js-sdk/src/models/event'; import BasicMessageComposer from "./BasicMessageComposer"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {CommandCategories, getCommand} from '../../../SlashCommands'; import {Action} from "../../../dispatcher/actions"; import CountlyAnalytics from "../../../CountlyAnalytics"; import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import SendHistoryManager from '../../../SendHistoryManager'; +import Modal from '../../../Modal'; function _isReply(mxEvent) { const relatesTo = mxEvent.getContent()["m.relates_to"]; @@ -120,6 +123,7 @@ export default class EditMessageComposer extends React.Component { saveDisabled: true, }; this._createEditorModel(); + window.addEventListener("beforeunload", this._saveStoredEditorState); } _setEditorRef = ref => { @@ -164,6 +168,7 @@ export default class EditMessageComposer extends React.Component { if (nextEvent) { dis.dispatch({action: 'edit_event', event: nextEvent}); } else { + this._clearStoredEditorState(); dis.dispatch({action: 'edit_event', event: null}); dis.fire(Action.FocusComposer); } @@ -173,11 +178,71 @@ export default class EditMessageComposer extends React.Component { } } + get _editorRoomKey() { + return `mx_edit_room_${this._getRoom().roomId}`; + } + + get _editorStateKey() { + return `mx_edit_state_${this.props.editState.getEvent().getId()}`; + } + _cancelEdit = () => { + this._clearStoredEditorState(); dis.dispatch({action: "edit_event", event: null}); dis.fire(Action.FocusComposer); } + get _shouldSaveStoredEditorState() { + return localStorage.getItem(this._editorRoomKey) !== null; + } + + _restoreStoredEditorState(partCreator) { + const json = localStorage.getItem(this._editorStateKey); + if (json) { + try { + const {parts: serializedParts} = JSON.parse(json); + const parts = serializedParts.map(p => partCreator.deserializePart(p)); + return parts; + } catch (e) { + console.error("Error parsing editing state: ", e); + } + } + } + + _clearStoredEditorState() { + localStorage.removeItem(this._editorRoomKey); + localStorage.removeItem(this._editorStateKey); + } + + _clearPreviousEdit() { + if (localStorage.getItem(this._editorRoomKey)) { + localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this._editorRoomKey)}`); + } + } + + _saveStoredEditorState() { + const item = SendHistoryManager.createItem(this.model); + this._clearPreviousEdit(); + localStorage.setItem(this._editorRoomKey, this.props.editState.getEvent().getId()); + localStorage.setItem(this._editorStateKey, JSON.stringify(item)); + } + + _isSlashCommand() { + const parts = this.model.parts; + const firstPart = parts[0]; + if (firstPart) { + if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { + return true; + } + + if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") + && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + return true; + } + } + return false; + } + _isContentModified(newContent) { // if nothing has changed then bail const oldContent = this.props.editState.getEvent().getContent(); @@ -190,19 +255,114 @@ export default class EditMessageComposer extends React.Component { return true; } - _sendEdit = () => { + _getSlashCommand() { + const commandText = this.model.parts.reduce((text, part) => { + // use mxid to textify user pills in a command + if (part.type === "user-pill") { + return text + part.resourceId; + } + return text + part.text; + }, ""); + const {cmd, args} = getCommand(commandText); + return [cmd, args, commandText]; + } + + async _runSlashCommand(cmd, args, roomId) { + const result = cmd.run(roomId, args); + let messageContent; + let error = result.error; + if (result.promise) { + try { + if (cmd.category === CommandCategories.messages) { + messageContent = await result.promise; + } else { + await result.promise; + } + } catch (err) { + error = err; + } + } + if (error) { + console.error("Command failure: %s", error); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + // assume the error is a server error when the command is async + const isServerError = !!result.promise; + const title = isServerError ? _td("Server error") : _td("Command error"); + + let errText; + if (typeof error === 'string') { + errText = error; + } else if (error.message) { + errText = error.message; + } else { + errText = _t("Server unavailable, overloaded, or something else went wrong."); + } + + Modal.createTrackedDialog(title, '', ErrorDialog, { + title: _t(title), + description: errText, + }); + } else { + console.log("Command success."); + if (messageContent) return messageContent; + } + } + + _sendEdit = async () => { const startTime = CountlyAnalytics.getTimestamp(); const editedEvent = this.props.editState.getEvent(); const editContent = createEditContent(this.model, editedEvent); const newContent = editContent["m.new_content"]; + let shouldSend = true; + // If content is modified then send an updated event into the room if (this._isContentModified(newContent)) { const roomId = editedEvent.getRoomId(); - this._cancelPreviousPendingEdit(); - const prom = this.context.sendMessage(roomId, editContent); - dis.dispatch({action: "message_sent"}); - CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent); + if (!containsEmote(this.model) && this._isSlashCommand()) { + const [cmd, args, commandText] = this._getSlashCommand(); + if (cmd) { + if (cmd.category === CommandCategories.messages) { + editContent["m.new_content"] = await this._runSlashCommand(cmd, args, roomId); + } else { + this._runSlashCommand(cmd, args, roomId); + shouldSend = false; + } + } else { + // ask the user if their unknown command should be sent as a message + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { + title: _t("Unknown Command"), + description:
    +

    + { _t("Unrecognised command: %(commandText)s", {commandText}) } +

    +

    + { _t("You can use /help to list available commands. " + + "Did you mean to send this as a message?", {}, { + code: t => { t }, + }) } +

    +

    + { _t("Hint: Begin your message with // to start it with a slash.", {}, { + code: t => { t }, + }) } +

    +
    , + button: _t('Send as message'), + }); + const [sendAnyway] = await finished; + // if !sendAnyway bail to let the user edit the composer and try again + if (!sendAnyway) return; + } + } + if (shouldSend) { + this._cancelPreviousPendingEdit(); + const prom = this.context.sendMessage(roomId, editContent); + this._clearStoredEditorState(); + dis.dispatch({action: "message_sent"}); + CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent); + } } // close the event editing and focus composer @@ -235,22 +395,27 @@ export default class EditMessageComposer extends React.Component { // then when mounting the editor again with the same editor state, // it will set the cursor at the end. this.props.editState.setEditorState(caret, parts); + window.removeEventListener("beforeunload", this._saveStoredEditorState); + if (this._shouldSaveStoredEditorState) { + this._saveStoredEditorState(); + } } _createEditorModel() { const {editState} = this.props; const room = this._getRoom(); - const partCreator = new PartCreator(room, this.context); + const partCreator = new CommandPartCreator(room, this.context); let parts; if (editState.hasEditorState()) { // if restoring state from a previous editor, // restore serialized parts from the state parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p)); } else { - // otherwise, parse the body of the event - parts = parseEvent(editState.getEvent(), partCreator); + //otherwise, either restore serialized parts from localStorage or parse the body of the event + parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator); } this.model = new EditorModel(parts, partCreator); + this._saveStoredEditorState(); } _getInitialCaretPosition() { diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 19c5a7acaa..8cec067c39 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -277,6 +277,12 @@ interface IProps { // Helper to build permalinks for the room permalinkCreator?: RoomPermalinkCreator; + + // Symbol of the root node + as?: string + + // whether or not to always show timestamps + alwaysShowTimestamps?: boolean } interface IState { @@ -291,12 +297,15 @@ interface IState { previouslyRequestedKeys: boolean; // The Relations model from the JS SDK for reactions to `mxEvent` reactions: Relations; + + hover: boolean; } @replaceableComponent("views.rooms.EventTile") export default class EventTile extends React.Component { private suppressReadReceiptAnimation: boolean; private isListeningForReceipts: boolean; + private ref: React.RefObject; private tile = React.createRef(); private replyThread = React.createRef(); @@ -322,6 +331,8 @@ export default class EventTile extends React.Component { previouslyRequestedKeys: false, // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), + + hover: false, }; // don't do RR animations until we are mounted @@ -333,6 +344,8 @@ export default class EventTile extends React.Component { // to determine if we've already subscribed and use a combination of other flags to find // out if we should even be subscribed at all. this.isListeningForReceipts = false; + + this.ref = React.createRef(); } /** @@ -631,7 +644,7 @@ export default class EventTile extends React.Component { // return early if there are no read receipts if (!this.props.readReceipts || this.props.readReceipts.length === 0) { - return (); + return null; } const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); @@ -640,6 +653,11 @@ export default class EventTile extends React.Component { let left = 0; const receipts = this.props.readReceipts || []; + + if (receipts.length === 0) { + return null; + } + for (let i = 0; i < receipts.length; ++i) { const receipt = receipts[i]; @@ -690,10 +708,14 @@ export default class EventTile extends React.Component { } } - return - { remText } - { avatars } - ; + return ( +
    + + { remText } + { avatars } + +
    + ) } onSenderProfileClick = event => { @@ -790,13 +812,6 @@ export default class EventTile extends React.Component { return null; } const eventId = this.props.mxEvent.getId(); - if (!eventId) { - // XXX: Temporary diagnostic logging for https://github.com/vector-im/element-web/issues/11120 - console.error("EventTile attempted to get relations for an event without an ID"); - // Use event's special `toJSON` method to log key data. - console.log(JSON.stringify(this.props.mxEvent, null, 4)); - console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120"); - } return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction"); }; @@ -960,7 +975,8 @@ export default class EventTile extends React.Component { onFocusChange={this.onActionBarFocusChange} /> : undefined; - const timestamp = this.props.mxEvent.getTs() ? + const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover); + const timestamp = showTimestamp ? : null; const keyRequestHelpText = @@ -1023,11 +1039,7 @@ export default class EventTile extends React.Component { let msgOption; if (this.props.showReadReceipts) { const readAvatars = this.getReadAvatars(); - msgOption = ( -
    - { readAvatars } -
    - ); + msgOption = readAvatars; } switch (this.props.tileShape) { @@ -1131,11 +1143,20 @@ export default class EventTile extends React.Component { // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( -
    - { ircTimestamp } - { sender } - { ircPadlock } -
    + React.createElement(this.props.as || "div", { + "ref": this.ref, + "className": classes, + "tabIndex": -1, + "aria-live": ariaLive, + "aria-atomic": "true", + "data-scroll-tokens": this.props["data-scroll-tokens"], + "onMouseEnter": () => this.setState({ hover: true }), + "onMouseLeave": () => this.setState({ hover: false }), + }, [ + ircTimestamp, + sender, + ircPadlock, +
    { groupTimestamp } { groupPadlock } { thread } @@ -1152,16 +1173,12 @@ export default class EventTile extends React.Component { { keyRequestInfo } { reactionsRow } { actionBar } -
    - {msgOption} - { - // The avatar goes after the event tile as it's absolutely positioned to be over the - // event tile line, so needs to be later in the DOM so it appears on top (this avoids - // the need for further z-indexing chaos) - } - { avatar } -
    - ); +
    , + msgOption, + avatar, + + ]) + ) } } } @@ -1323,11 +1340,15 @@ class SentReceipt extends React.PureComponent; } - return - - {nonCssBadge} - {tooltip} - - ; + return ( +
    + + + {nonCssBadge} + {tooltip} + + +
    + ); } } diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index fbc0e477a1..cb50f0fff3 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -133,6 +133,12 @@ export default class MemberList extends React.Component { } } + get canInvite() { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(this.props.roomId); + return room && room.canInvite(cli.getUserId()); + } + _getMembersState(members) { // set the state after determining _showPresence to make sure it's // taken into account while rerendering @@ -141,6 +147,7 @@ export default class MemberList extends React.Component { members: members, filteredJoinedMembers: this._filterMembers(members, 'join'), filteredInvitedMembers: this._filterMembers(members, 'invite'), + canInvite: this.canInvite, // ideally we'd size this to the page height, but // in practice I find that a little constraining @@ -196,6 +203,8 @@ export default class MemberList extends React.Component { event.getType() === "m.room.third_party_invite") { this._updateList(); } + + if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite }); }; _updateList = rate_limited_func(() => { @@ -229,6 +238,8 @@ export default class MemberList extends React.Component { member.user = cli.getUser(member.userId); } + member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""); + // XXX: this user may have no lastPresenceTs value! // the right solution here is to fix the race rather than leave it as 0 }); @@ -243,6 +254,8 @@ export default class MemberList extends React.Component { m.membership === 'join' || m.membership === 'invite' ); }); + const language = SettingsStore.getValue("language"); + this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true }); filteredAndSortedMembers.sort(this.memberSort); return filteredAndSortedMembers; } @@ -342,13 +355,7 @@ export default class MemberList extends React.Component { } // Fourth by name (alphabetical) - const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, ""); - const nameB = (memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name).replace(SORT_REGEX, ""); - // console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`); - return nameA.localeCompare(nameB, { - ignorePunctuation: true, - sensitivity: "base", - }); + return this.collator.compare(memberA.sortName, memberB.sortName); }; onSearchQueryChanged = searchQuery => { @@ -413,7 +420,7 @@ export default class MemberList extends React.Component { } else { // Is a 3pid invite return this._onPending3pidInviteClick(m)} />; + onClick={() => this._onPending3pidInviteClick(m)} />; } }); } @@ -455,8 +462,6 @@ export default class MemberList extends React.Component { let inviteButton; if (room && room.getMyMembership() === 'join') { - const canInvite = room.canInvite(cli.getUserId()); - let inviteButtonText = _t("Invite to this room"); const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat(); if (chat && chat.roomId === this.props.roomId) { @@ -467,7 +472,7 @@ export default class MemberList extends React.Component { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); inviteButton = - + { inviteButtonText } ; } @@ -477,10 +482,10 @@ export default class MemberList extends React.Component { if (this._getChildCountInvited() > 0) { invitedHeader =

    { _t("Invited") }

    ; invitedSection = ; + createOverflowElement={this._createOverflowTileInvited} + getChildren={this._getChildrenInvited} + getChildCount={this._getChildCountInvited} + />; } const footer = ( @@ -513,9 +518,9 @@ export default class MemberList extends React.Component { >
    + createOverflowElement={this._createOverflowTileJoined} + getChildren={this._getChildrenJoined} + getChildCount={this._getChildCountJoined} /> { invitedHeader } { invitedSection }
    diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.js deleted file mode 100644 index 78cf422cc6..0000000000 --- a/src/components/views/rooms/PinnedEventTile.js +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2017 Travis Ralston - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import PropTypes from 'prop-types'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import dis from "../../../dispatcher/dispatcher"; -import AccessibleButton from "../elements/AccessibleButton"; -import MessageEvent from "../messages/MessageEvent"; -import MemberAvatar from "../avatars/MemberAvatar"; -import { _t } from '../../../languageHandler'; -import {formatFullDate} from '../../../DateUtils'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; - -@replaceableComponent("views.rooms.PinnedEventTile") -export default class PinnedEventTile extends React.Component { - static propTypes = { - mxRoom: PropTypes.object.isRequired, - mxEvent: PropTypes.object.isRequired, - onUnpinned: PropTypes.func, - }; - - onTileClicked = () => { - dis.dispatch({ - action: 'view_room', - event_id: this.props.mxEvent.getId(), - highlighted: true, - room_id: this.props.mxEvent.getRoomId(), - }); - }; - - onUnpinClicked = () => { - const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", ""); - if (!pinnedEvents || !pinnedEvents.getContent().pinned) { - // Nothing to do: already unpinned - if (this.props.onUnpinned) this.props.onUnpinned(); - } else { - const pinned = pinnedEvents.getContent().pinned; - const index = pinned.indexOf(this.props.mxEvent.getId()); - if (index !== -1) { - pinned.splice(index, 1); - MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '') - .then(() => { - if (this.props.onUnpinned) this.props.onUnpinned(); - }); - } else if (this.props.onUnpinned) this.props.onUnpinned(); - } - }; - - _canUnpin() { - return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get()); - } - - render() { - const sender = this.props.mxEvent.getSender(); - // Get the latest sender profile rather than historical - const senderProfile = this.props.mxRoom.getMember(sender); - const avatarSize = 40; - - let unpinButton = null; - if (this._canUnpin()) { - unpinButton = ( - - {_t('Unpin - - ); - } - - return ( -
    -
    - - { _t("Jump to message") } - - { unpinButton } -
    - - - - - - { senderProfile ? senderProfile.name : sender } - - - { formatFullDate(new Date(this.props.mxEvent.getTs())) } - -
    - {}} // we need to give this, apparently - /> -
    -
    - ); - } -} diff --git a/src/components/views/rooms/PinnedEventTile.tsx b/src/components/views/rooms/PinnedEventTile.tsx new file mode 100644 index 0000000000..774dea70c8 --- /dev/null +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -0,0 +1,104 @@ +/* +Copyright 2017 Travis Ralston +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import dis from "../../../dispatcher/dispatcher"; +import AccessibleButton from "../elements/AccessibleButton"; +import MessageEvent from "../messages/MessageEvent"; +import MemberAvatar from "../avatars/MemberAvatar"; +import { _t } from '../../../languageHandler'; +import { formatDate } from '../../../DateUtils'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { getUserNameColorClass } from "../../../utils/FormattingUtils"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; + +interface IProps { + room: Room; + event: MatrixEvent; + onUnpinClicked?(): void; +} + +const AVATAR_SIZE = 24; + +@replaceableComponent("views.rooms.PinnedEventTile") +export default class PinnedEventTile extends React.Component { + public static contextType = MatrixClientContext; + + private onTileClicked = () => { + dis.dispatch({ + action: 'view_room', + event_id: this.props.event.getId(), + highlighted: true, + room_id: this.props.event.getRoomId(), + }); + }; + + render() { + const sender = this.props.event.getSender(); + const senderProfile = this.props.room.getMember(sender); + + let unpinButton = null; + if (this.props.onUnpinClicked) { + unpinButton = ( + + ); + } + + return
    + + + + { senderProfile?.name || sender } + + + { unpinButton } + +
    + {}} // we need to give this, apparently + /> +
    + +
    + + { formatDate(new Date(this.props.event.getTs())) } + + + + { _t("View message") } + +
    +
    ; + } +} diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js deleted file mode 100644 index 4b310dbbca..0000000000 --- a/src/components/views/rooms/PinnedEventsPanel.js +++ /dev/null @@ -1,145 +0,0 @@ -/* -Copyright 2017 Travis Ralston -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import PropTypes from 'prop-types'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import AccessibleButton from "../elements/AccessibleButton"; -import PinnedEventTile from "./PinnedEventTile"; -import { _t } from '../../../languageHandler'; -import PinningUtils from "../../../utils/PinningUtils"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; - -@replaceableComponent("views.rooms.PinnedEventsPanel") -export default class PinnedEventsPanel extends React.Component { - static propTypes = { - // The Room from the js-sdk we're going to show pinned events for - room: PropTypes.object.isRequired, - - onCancelClick: PropTypes.func, - }; - - state = { - loading: true, - }; - - componentDidMount() { - this._updatePinnedMessages(); - MatrixClientPeg.get().on("RoomState.events", this._onStateEvent); - } - - componentWillUnmount() { - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent); - } - } - - _onStateEvent = ev => { - if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") { - this._updatePinnedMessages(); - } - }; - - _updatePinnedMessages = () => { - const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", ""); - if (!pinnedEvents || !pinnedEvents.getContent().pinned) { - this.setState({ loading: false, pinned: [] }); - } else { - const promises = []; - const cli = MatrixClientPeg.get(); - - pinnedEvents.getContent().pinned.map((eventId) => { - promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then( - (timeline) => { - const event = timeline.getEvents().find((e) => e.getId() === eventId); - return {eventId, timeline, event}; - }).catch((err) => { - console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId); - console.error(err); - return null; // return lack of context to avoid unhandled errors - })); - }); - - Promise.all(promises).then((contexts) => { - // Filter out the messages before we try to render them - const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event)); - - this.setState({ loading: false, pinned }); - }); - } - - this._updateReadState(); - }; - - _updateReadState() { - const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", ""); - if (!pinnedEvents) return; // nothing to read - - let readStateEvents = []; - const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); - if (readPinsEvent && readPinsEvent.getContent()) { - readStateEvents = readPinsEvent.getContent().event_ids || []; - } - - if (!readStateEvents.includes(pinnedEvents.getId())) { - readStateEvents.push(pinnedEvents.getId()); - - // Only keep the last 10 event IDs to avoid infinite growth - readStateEvents = readStateEvents.reverse().splice(0, 10).reverse(); - - MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", { - event_ids: readStateEvents, - }); - } - } - - _getPinnedTiles() { - if (this.state.pinned.length === 0) { - return (
    { _t("No pinned messages.") }
    ); - } - - return this.state.pinned.map((context) => { - return ( - - ); - }); - } - - render() { - let tiles =
    { _t("Loading...") }
    ; - if (this.state && !this.state.loading) { - tiles = this._getPinnedTiles(); - } - - return ( -
    -
    - - - -

    { _t("Pinned Messages") }

    - { tiles } -
    -
    - ); - } -} diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx index ea0ff233da..00f13209a2 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs.tsx @@ -23,11 +23,9 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import Analytics from "../../../Analytics"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { CSSTransition } from "react-transition-group"; -import RoomListStore from "../../../stores/room-list/RoomListStore"; -import { DefaultTagID } from "../../../stores/room-list/models"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import Toolbar from "../../../accessibility/Toolbar"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { } @@ -84,8 +82,6 @@ export default class RoomBreadcrumbs extends React.PureComponent public render(): React.ReactElement { const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => { - const roomTags = RoomListStore.instance.getTagsForRoom(r); - const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0]; return ( diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index f856f7f6ef..cb6bb0afca 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -19,20 +19,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import RateLimitedFunc from '../../../ratelimitedfunc'; -import {CancelButton} from './SimpleRoomHeader'; +import { CancelButton } from './SimpleRoomHeader'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import E2EIcon from './E2EIcon'; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; -import {DefaultTagID} from "../../../stores/room-list/models"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import RoomTopic from "../elements/RoomTopic"; import RoomName from "../elements/RoomName"; -import {PlaceCallType} from "../../../CallHandler"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { PlaceCallType } from "../../../CallHandler"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.rooms.RoomHeader") export default class RoomHeader extends React.Component { @@ -41,7 +40,6 @@ export default class RoomHeader extends React.Component { oobData: PropTypes.object, inRoom: PropTypes.bool, onSettingsClick: PropTypes.func, - onPinnedClick: PropTypes.func, onSearchClick: PropTypes.func, onLeaveClick: PropTypes.func, onCancelClick: PropTypes.func, @@ -60,14 +58,12 @@ export default class RoomHeader extends React.Component { componentDidMount() { const cli = MatrixClientPeg.get(); cli.on("RoomState.events", this._onRoomStateEvents); - cli.on("Room.accountData", this._onRoomAccountData); } componentWillUnmount() { const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.events", this._onRoomStateEvents); - cli.removeListener("Room.accountData", this._onRoomAccountData); } } @@ -80,48 +76,14 @@ export default class RoomHeader extends React.Component { this._rateLimitedUpdate(); }; - _onRoomAccountData = (event, room) => { - if (!this.props.room || room.roomId !== this.props.room.roomId) return; - if (event.getType() !== "im.vector.room.read_pins") return; - - this._rateLimitedUpdate(); - }; - _rateLimitedUpdate = new RateLimitedFunc(function() { /* eslint-disable babel/no-invalid-this */ this.forceUpdate(); }, 500); - _hasUnreadPins() { - const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); - if (!currentPinEvent) return false; - if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) { - return false; // no pins == nothing to read - } - - const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); - if (readPinsEvent && readPinsEvent.getContent()) { - const readStateEvents = readPinsEvent.getContent().event_ids || []; - if (readStateEvents) { - return !readStateEvents.includes(currentPinEvent.getId()); - } - } - - // There's pins, and we haven't read any of them - return true; - } - - _hasPins() { - const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); - if (!currentPinEvent) return false; - - return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0); - } - render() { let searchStatus = null; let cancelButton = null; - let pinnedEventsButton = null; if (this.props.onCancelClick) { cancelButton = ; @@ -177,30 +139,11 @@ export default class RoomHeader extends React.Component { roomAvatar = ; } - if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) { - let pinsIndicator = null; - if (this._hasUnreadPins()) { - pinsIndicator = (
    ); - } else if (this._hasPins()) { - pinsIndicator = (
    ); - } - - pinnedEventsButton = - - { pinsIndicator } - ; - } - let forgetButton; if (this.props.onForgetClick) { forgetButton = @@ -250,7 +193,6 @@ export default class RoomHeader extends React.Component {
    { videoCallButton } { voiceCallButton } - { pinnedEventsButton } { forgetButton } { appsButton } { searchButton } @@ -267,7 +209,7 @@ export default class RoomHeader extends React.Component { { topicElement } { cancelButton } { rightRow } - +
    ); diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index fdf38784cf..8b36341ed0 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -46,17 +46,16 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con import AccessibleButton from "../elements/AccessibleButton"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import CallHandler from "../../../CallHandler"; -import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore"; +import SpaceStore, {ISuggestedRoom, SUGGESTED_ROOMS} from "../../../stores/SpaceStore"; import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import RoomAvatar from "../avatars/RoomAvatar"; -import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; onFocus: (ev: React.FocusEvent) => void; onBlur: (ev: React.FocusEvent) => void; - onResize: () => void; + onListCollapse?: (isExpanded: boolean) => void; resizeNotifier: ResizeNotifier; isMinimized: boolean; activeSpace: Room; @@ -66,7 +65,7 @@ interface IState { sublists: ITagMap; isNameFiltering: boolean; currentRoomId?: string; - suggestedRooms: ISpaceSummaryRoom[]; + suggestedRooms: ISuggestedRoom[]; } const TAG_ORDER: TagID[] = [ @@ -363,7 +362,7 @@ export default class RoomList extends React.PureComponent { return room; }; - private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => { + private updateSuggestedRooms = (suggestedRooms: ISuggestedRoom[]) => { this.setState({ suggestedRooms }); }; @@ -405,9 +404,7 @@ export default class RoomList extends React.PureComponent { const newSublists = objectWithOnly(newLists, newListIds); const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v)); - this.setState({sublists, isNameFiltering}, () => { - this.props.onResize(); - }); + this.setState({sublists, isNameFiltering}); } }; @@ -428,7 +425,7 @@ export default class RoomList extends React.PureComponent { private renderSuggestedRooms(): ReactComponentElement[] { return this.state.suggestedRooms.map(room => { - const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"); + const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room"); const avatar = ( { const viewRoom = () => { defaultDispatcher.dispatch({ action: "view_room", + room_alias: room.canonical_alias || room.aliases?.[0], room_id: room.room_id, + via_servers: room.viaServers, oobData: { avatarUrl: room.avatar_url, name, @@ -536,11 +535,11 @@ export default class RoomList extends React.PureComponent { addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel} addRoomContextMenu={aesthetics.addRoomContextMenu} isMinimized={this.props.isMinimized} - onResize={this.props.onResize} showSkeleton={showSkeleton} extraTiles={extraTiles} resizeNotifier={this.props.resizeNotifier} alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)} + onListCollapse={this.props.onListCollapse} /> }); } diff --git a/src/components/views/rooms/RoomListNumResults.tsx b/src/components/views/rooms/RoomListNumResults.tsx index 01cbecf05d..19023e361c 100644 --- a/src/components/views/rooms/RoomListNumResults.tsx +++ b/src/components/views/rooms/RoomListNumResults.tsx @@ -14,14 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useState} from "react"; +import React, {useEffect, useState} from "react"; import { _t } from "../../../languageHandler"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import SpaceStore from "../../../stores/SpaceStore"; -const RoomListNumResults: React.FC = () => { +interface IProps { + onVisibilityChange?: () => void +} + +const RoomListNumResults: React.FC = ({ onVisibilityChange }) => { const [count, setCount] = useState(null); useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => { if (RoomListStore.instance.getFirstNameFilterCondition()) { @@ -32,6 +36,12 @@ const RoomListNumResults: React.FC = () => { } }); + useEffect(() => { + if (onVisibilityChange) { + onVisibilityChange(); + } + }, [count, onVisibilityChange]); + if (typeof count !== "number") return null; return
    diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index dd80e9d40a..0bb7381dbc 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -18,6 +18,7 @@ limitations under the License. import * as React from "react"; import { createRef, ReactComponentElement } from "react"; +import { normalize } from "matrix-js-sdk/src/utils"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from 'classnames'; import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; @@ -73,11 +74,11 @@ interface IProps { addRoomLabel: string; isMinimized: boolean; tagId: TagID; - onResize: () => void; showSkeleton?: boolean; alwaysVisible?: boolean; resizeNotifier: ResizeNotifier; extraTiles?: ReactComponentElement[]; + onListCollapse?: (isExpanded: boolean) => void; // TODO: Account for https://github.com/vector-im/element-web/issues/14179 } @@ -104,6 +105,7 @@ interface IState { export default class RoomSublist extends React.Component { private headerButton = createRef(); private sublistRef = createRef(); + private tilesRef = createRef(); private dispatcherRef: string; private layout: ListLayout; private heightAtStart: number; @@ -245,11 +247,15 @@ export default class RoomSublist extends React.Component { public componentDidMount() { this.dispatcherRef = defaultDispatcher.register(this.onAction); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated); + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + this.tilesRef.current?.addEventListener("scroll", this.onScrollPrevent, { passive: true }); } public componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated); + this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent); } private onListsUpdated = () => { @@ -259,7 +265,7 @@ export default class RoomSublist extends React.Component { const nameCondition = RoomListStore.instance.getFirstNameFilterCondition(); if (nameCondition) { stateUpdates.filteredExtraTiles = this.props.extraTiles - .filter(t => nameCondition.matches(t.props.displayName || "")); + .filter(t => nameCondition.matches(normalize(t.props.displayName || ""))); } else if (this.state.filteredExtraTiles) { stateUpdates.filteredExtraTiles = null; } @@ -472,7 +478,9 @@ export default class RoomSublist extends React.Component { private toggleCollapsed = () => { this.layout.isCollapsed = this.state.isExpanded; this.setState({isExpanded: !this.layout.isCollapsed}); - setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated + if (this.props.onListCollapse) { + this.props.onListCollapse(!this.layout.isCollapsed) + } }; private onHeaderKeyDown = (ev: React.KeyboardEvent) => { @@ -529,7 +537,6 @@ export default class RoomSublist extends React.Component { tiles.push( { ); } - private onScrollPrevent(e: React.UIEvent) { + private onScrollPrevent(e: Event) { // the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable // this fixes https://github.com/vector-im/element-web/issues/14413 (e.target as HTMLDivElement).scrollTop = 0; @@ -882,7 +889,7 @@ export default class RoomSublist extends React.Component { className="mx_RoomSublist_resizeBox" enable={handles} > -
    +
    {visibleTiles}
    {showNButton} diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 7303f36489..aae182eca4 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -53,14 +53,12 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community import { replaceableComponent } from "../../../utils/replaceableComponent"; import { getUnsentMessages } from "../../structures/RoomStatusBar"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; -import { ResizeNotifier } from "../../../utils/ResizeNotifier"; interface IProps { room: Room; showMessagePreview: boolean; isMinimized: boolean; tag: TagID; - resizeNotifier: ResizeNotifier; } type PartialDOMRect = Pick; @@ -100,13 +98,12 @@ export default class RoomTile extends React.PureComponent { hasUnsentEvents: this.countUnsentEvents() > 0, // generatePreview() will return nothing if the user has previews disabled - messagePreview: this.generatePreview(), + messagePreview: "", }; + this.generatePreview(); + this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room); this.roomProps = EchoChamber.forRoom(this.props.room); - if (this.props.resizeNotifier) { - this.props.resizeNotifier.on("middlePanelResized", this.onResize); - } } private countUnsentEvents(): number { @@ -121,12 +118,6 @@ export default class RoomTile extends React.PureComponent { this.forceUpdate(); // notification state changed - update }; - private onResize = () => { - if (this.showMessagePreview && !this.state.messagePreview) { - this.setState({messagePreview: this.generatePreview()}); - } - }; - private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => { if (!room?.roomId === this.props.room.roomId) return; this.setState({hasUnsentEvents: this.countUnsentEvents() > 0}); @@ -146,8 +137,10 @@ export default class RoomTile extends React.PureComponent { } public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { - if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) { - this.setState({messagePreview: this.generatePreview()}); + const showMessageChanged = prevProps.showMessagePreview !== this.props.showMessagePreview; + const minimizedChanged = prevProps.isMinimized !== this.props.isMinimized; + if (showMessageChanged || minimizedChanged) { + this.generatePreview(); } if (prevProps.room?.roomId !== this.props.room?.roomId) { MessagePreviewStore.instance.off( @@ -206,9 +199,6 @@ export default class RoomTile extends React.PureComponent { ); this.props.room.off("Room.name", this.onRoomNameUpdate); } - if (this.props.resizeNotifier) { - this.props.resizeNotifier.off("middlePanelResized", this.onResize); - } ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); defaultDispatcher.unregister(this.dispatcherRef); this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); @@ -236,17 +226,17 @@ export default class RoomTile extends React.PureComponent { private onRoomPreviewChanged = (room: Room) => { if (this.props.room && room.roomId === this.props.room.roomId) { - // generatePreview() will return nothing if the user has previews disabled - this.setState({messagePreview: this.generatePreview()}); + this.generatePreview(); } }; - private generatePreview(): string | null { + private async generatePreview() { if (!this.showMessagePreview) { return null; } - return MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag); + const messagePreview = await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag); + this.setState({ messagePreview }); } private scrollIntoView = () => { @@ -576,7 +566,6 @@ export default class RoomTile extends React.PureComponent { const roomAvatar = ; diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index b2a66f6670..9aedb38654 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -62,13 +62,11 @@ export default class SimpleRoomHeader extends React.Component { } return ( -
    -
    -
    - { icon } - { this.props.title } - { cancelButton } -
    +
    +
    + { icon } + { this.props.title } + { cancelButton }
    ); diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 82e8cf640c..3d2300b83c 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -40,7 +40,7 @@ const STICKERPICKER_Z_INDEX = 3500; const PERSISTED_ELEMENT_KEY = "stickerPicker"; @replaceableComponent("views.rooms.Stickerpicker") -export default class Stickerpicker extends React.Component { +export default class Stickerpicker extends React.PureComponent { static currentWidget; constructor(props) { @@ -341,21 +341,27 @@ export default class Stickerpicker extends React.Component { * @param {Event} ev Event that triggered the function call */ _onHideStickersClick(ev) { - this.setState({showStickers: false}); + if (this.state.showStickers) { + this.setState({showStickers: false}); + } } /** * Called when the window is resized */ _onResize() { - this.setState({showStickers: false}); + if (this.state.showStickers) { + this.setState({showStickers: false}); + } } /** * The stickers picker was hidden */ _onFinished() { - this.setState({showStickers: false}); + if (this.state.showStickers) { + this.setState({showStickers: false}); + } } /** diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.tsx similarity index 75% rename from src/components/views/rooms/WhoIsTypingTile.js rename to src/components/views/rooms/WhoIsTypingTile.tsx index a25b43fc3a..3a1d2051b4 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -16,36 +16,45 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import Room from "matrix-js-sdk/src/models/room"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import * as WhoIsTyping from '../../../WhoIsTyping'; import Timer from '../../../utils/Timer'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import MemberAvatar from '../avatars/MemberAvatar'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { compare } from "../../../utils/strings"; + +interface IProps { + // the room this statusbar is representing. + room: Room; + onShown?: () => void; + onHidden?: () => void; + // Number of names to display in typing indication. E.g. set to 3, will + // result in "X, Y, Z and 100 others are typing." + whoIsTypingLimit: number; +} + +interface IState { + usersTyping: RoomMember[]; + // a map with userid => Timer to delay + // hiding the "x is typing" message for a + // user so hiding it can coincide + // with the sent message by the other side + // resulting in less timeline jumpiness + delayedStopTypingTimers: Record; +} @replaceableComponent("views.rooms.WhoIsTypingTile") -export default class WhoIsTypingTile extends React.Component { - static propTypes = { - // the room this statusbar is representing. - room: PropTypes.object.isRequired, - onShown: PropTypes.func, - onHidden: PropTypes.func, - // Number of names to display in typing indication. E.g. set to 3, will - // result in "X, Y, Z and 100 others are typing." - whoIsTypingLimit: PropTypes.number, - }; - +export default class WhoIsTypingTile extends React.Component { static defaultProps = { whoIsTypingLimit: 3, }; state = { usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), - // a map with userid => Timer to delay - // hiding the "x is typing" message for a - // user so hiding it can coincide - // with the sent message by the other side - // resulting in less timeline jumpiness delayedStopTypingTimers: {}, }; @@ -71,37 +80,39 @@ export default class WhoIsTypingTile extends React.Component { client.removeListener("RoomMember.typing", this.onRoomMemberTyping); client.removeListener("Room.timeline", this.onRoomTimeline); } - Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort()); + Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort()); } - _isVisible(state) { + private _isVisible(state: IState): boolean { return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0; } - isVisible = () => { + public isVisible = (): boolean => { return this._isVisible(this.state); }; - onRoomTimeline = (event, room) => { + private onRoomTimeline = (event: MatrixEvent, room: Room): void => { if (room?.roomId === this.props.room?.roomId) { const userId = event.getSender(); // remove user from usersTyping const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId); - this.setState({usersTyping}); + if (usersTyping.length !== this.state.usersTyping.length) { + this.setState({usersTyping}); + } // abort timer if any - this._abortUserTimer(userId); + this.abortUserTimer(userId); } }; - onRoomMemberTyping = (ev, member) => { + private onRoomMemberTyping = (): void => { const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room); this.setState({ - delayedStopTypingTimers: this._updateDelayedStopTypingTimers(usersTyping), + delayedStopTypingTimers: this.updateDelayedStopTypingTimers(usersTyping), usersTyping, }); }; - _updateDelayedStopTypingTimers(usersTyping) { + private updateDelayedStopTypingTimers(usersTyping: RoomMember[]): Record { const usersThatStoppedTyping = this.state.usersTyping.filter((a) => { return !usersTyping.some((b) => a.userId === b.userId); }); @@ -129,7 +140,7 @@ export default class WhoIsTypingTile extends React.Component { delayedStopTypingTimers[m.userId] = timer; timer.start(); timer.finished().then( - () => this._removeUserTimer(m.userId), // on elapsed + () => this.removeUserTimer(m.userId), // on elapsed () => {/* aborted */}, ); } @@ -139,15 +150,15 @@ export default class WhoIsTypingTile extends React.Component { return delayedStopTypingTimers; } - _abortUserTimer(userId) { + private abortUserTimer(userId: string): void { const timer = this.state.delayedStopTypingTimers[userId]; if (timer) { timer.abort(); - this._removeUserTimer(userId); + this.removeUserTimer(userId); } } - _removeUserTimer(userId) { + private removeUserTimer(userId: string): void { const timer = this.state.delayedStopTypingTimers[userId]; if (timer) { const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers); @@ -156,7 +167,7 @@ export default class WhoIsTypingTile extends React.Component { } } - _renderTypingIndicatorAvatars(users, limit) { + private renderTypingIndicatorAvatars(users: RoomMember[], limit: number): JSX.Element[] { let othersCount = 0; if (users.length > limit) { othersCount = users.length - limit + 1; @@ -197,20 +208,20 @@ export default class WhoIsTypingTile extends React.Component { usersTyping = usersTyping.concat(stoppedUsersOnTimer); // sort them so the typing members don't change order when // moved to delayedStopTypingTimers - usersTyping.sort((a, b) => a.name.localeCompare(b.name)); + usersTyping.sort((a, b) => compare(a.name, b.name)); const typingString = WhoIsTyping.whoIsTypingString( usersTyping, this.props.whoIsTypingLimit, ); if (!typingString) { - return (
    ); + return null; } return (
  • - { this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) } + { this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
    { typingString } diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index c0f23cb906..0cd1a64ada 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent { async _getUpdatedStatus() { const cli = MatrixClientPeg.get(); const pkCache = cli.getCrossSigningCacheCallbacks(); - const crossSigning = cli._crypto._crossSigningInfo; - const secretStorage = cli._crypto._secretStorage; + const crossSigning = cli.crypto._crossSigningInfo; + const secretStorage = cli.crypto._secretStorage; const crossSigningPublicKeysOnDevice = crossSigning.getId(); const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master")); diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx index fa84063ee8..e693f45c5f 100644 --- a/src/components/views/settings/EventIndexPanel.tsx +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -232,7 +232,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {

    {this.state.enabling ? - : _t("Message search initilisation failed") + : _t("Message search initialisation failed") }

    {EventIndexPeg.error && ( diff --git a/src/components/views/settings/SecureBackupPanel.js b/src/components/views/settings/SecureBackupPanel.js index 310114c8af..4f3eb0bdf6 100644 --- a/src/components/views/settings/SecureBackupPanel.js +++ b/src/components/views/settings/SecureBackupPanel.js @@ -131,10 +131,10 @@ export default class SecureBackupPanel extends React.PureComponent { async _getUpdatedDiagnostics() { const cli = MatrixClientPeg.get(); - const secretStorage = cli._crypto._secretStorage; + const secretStorage = cli.crypto._secretStorage; const backupKeyStored = !!(await cli.isKeyBackupKeyStored()); - const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey(); + const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey(); const backupKeyCached = !!(backupKeyFromCache); const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array; const secretStorageKeyInAccount = await secretStorage.hasKey(); diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 4fa521f598..19ebe2a77e 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -25,6 +25,7 @@ import {EventType} from "matrix-js-sdk/src/@types/event"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomState } from "matrix-js-sdk/src/models/room-state"; +import { compare } from "../../../../../utils/strings"; const plEventsToLabels = { // These will be translated for us later. @@ -312,7 +313,7 @@ export default class RolesRoomSettingsTab extends React.Component { // comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive) const comparator = (a, b) => { const plDiff = userLevels[b.key] - userLevels[a.key]; - return plDiff !== 0 ? plDiff : a.key.toLocaleLowerCase().localeCompare(b.key.toLocaleLowerCase()); + return plDiff !== 0 ? plDiff : compare(a.key.toLocaleLowerCase(), b.key.toLocaleLowerCase()); }; privilegedUsers.sort(comparator); diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index bc40c36bda..9e27ed968e 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -35,9 +35,10 @@ import Field from '../../../elements/Field'; import EventTilePreview from '../../../elements/EventTilePreview'; import StyledRadioGroup from "../../../elements/StyledRadioGroup"; import { SettingLevel } from "../../../../../settings/SettingLevel"; -import {UIFeature} from "../../../../../settings/UIFeature"; -import {Layout} from "../../../../../settings/Layout"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { UIFeature } from "../../../../../settings/UIFeature"; +import { Layout } from "../../../../../settings/Layout"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import { compare } from "../../../../../utils/strings"; interface IProps { } @@ -295,7 +296,7 @@ export default class AppearanceUserSettingsTab extends React.Component ({id: p[0], name: p[1]})); // convert pairs to objects for code readability const builtInThemes = themes.filter(p => !p.id.startsWith("custom-")); const customThemes = themes.filter(p => !builtInThemes.includes(p)) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort((a, b) => compare(a.name, b.name)); const orderedThemes = [...builtInThemes, ...customThemes]; return (
    diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index f515f1862b..98148b19e0 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -22,6 +22,8 @@ import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import * as sdk from "../../../../../index"; import {SettingLevel} from "../../../../../settings/SettingLevel"; import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import SdkConfig from "../../../../../SdkConfig"; +import BetaCard from "../../../beta/BetaCard"; export class LabsSettingToggle extends React.Component { static propTypes = { @@ -48,14 +50,40 @@ export default class LabsUserSettingsTab extends React.Component { } render() { - const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); - const flags = SettingsStore.getFeatureSettingNames().map(f => ); + const features = SettingsStore.getFeatureSettingNames(); + const [labs, betas] = features.reduce((arr, f) => { + arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f); + return arr; + }, [[], []]); + + let betaSection; + if (betas.length) { + betaSection =
    + { betas.map(f => ) } +
    ; + } + + let labsSection; + if (SdkConfig.get()['showLabsSettings']) { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + const flags = labs.map(f => ); + + labsSection =
    + {flags} + + + + +
    ; + } + return ( -
    +
    {_t("Labs")}
    { - _t('Customise your experience with experimental labs features. ' + + _t('Feeling experimental? Labs are the best way to get things early, ' + + 'test out new features and help shape them before they actually launch. ' + 'Learn more.', {}, { 'a': (sub) => { return -
    - {flags} - - - - -
    + { betaSection } + { labsSection }
    ); } diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx index bc378ab956..ec40f7bed8 100644 --- a/src/components/views/spaces/SpaceBasicSettings.tsx +++ b/src/components/views/spaces/SpaceBasicSettings.tsx @@ -32,17 +32,11 @@ interface IProps { setTopic(topic: string): void; } -const SpaceBasicSettings = ({ +export const SpaceAvatar = ({ avatarUrl, avatarDisabled = false, setAvatar, - name = "", - nameDisabled = false, - setName, - topic = "", - topicDisabled = false, - setTopic, -}: IProps) => { +}: Pick) => { const avatarUploadRef = useRef(); const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache @@ -81,20 +75,34 @@ const SpaceBasicSettings = ({ } } + return
    + { avatarSection } + { + if (!e.target.files?.length) return; + const file = e.target.files[0]; + setAvatar(file); + const reader = new FileReader(); + reader.onload = (ev) => { + setAvatarDataUrl(ev.target.result as string); + }; + reader.readAsDataURL(file); + }} accept="image/*" /> +
    ; +}; + +const SpaceBasicSettings = ({ + avatarUrl, + avatarDisabled = false, + setAvatar, + name = "", + nameDisabled = false, + setName, + topic = "", + topicDisabled = false, + setTopic, +}: IProps) => { return
    -
    - { avatarSection } - { - if (!e.target.files?.length) return; - const file = e.target.files[0]; - setAvatar(file); - const reader = new FileReader(); - reader.onload = (ev) => { - setAvatarDataUrl(ev.target.result as string); - }; - reader.readAsDataURL(file); - }} accept="image/*" /> -
    + { return ( @@ -41,17 +48,39 @@ enum Visibility { Private, } +const spaceNameValidator = withValidation({ + rules: [ + { + key: "required", + test: async ({ value }) => !!value, + invalid: () => _t("Please enter a name for the space"), + }, + ], +}); + const SpaceCreateMenu = ({ onFinished }) => { const cli = useContext(MatrixClientContext); const [visibility, setVisibility] = useState(null); - const [name, setName] = useState(""); - const [avatar, setAvatar] = useState(null); - const [topic, setTopic] = useState(""); const [busy, setBusy] = useState(false); - const onSpaceCreateClick = async () => { + const [name, setName] = useState(""); + const spaceNameField = useRef(); + const [avatar, setAvatar] = useState(null); + const [topic, setTopic] = useState(""); + + const onSpaceCreateClick = async (e) => { + e.preventDefault(); if (busy) return; + setBusy(true); + // require & validate the space name field + if (!await spaceNameField.current.validate({ allowEmpty: false })) { + spaceNameField.current.focus(); + spaceNameField.current.validate({ allowEmpty: false, focused: true }); + setBusy(false); + return; + } + const initialState: IStateEvent[] = [ { type: EventType.RoomHistoryVisibility, @@ -107,7 +136,7 @@ const SpaceCreateMenu = ({ onFinished }) => { if (visibility === null) { body =

    { _t("Create a space") }

    -

    { _t("Spaces are new ways to group rooms and people. " + +

    { _t("Spaces are a new way to group rooms and people. " + "To join an existing space you'll need an invite.") }

    { />

    { _t("You can change this later") }

    + +
    ; } else { body = @@ -146,9 +177,32 @@ const SpaceCreateMenu = ({ onFinished }) => { }

    - + + - + setName(ev.target.value)} + ref={spaceNameField} + onValidate={spaceNameValidator} + disabled={busy} + /> + + setTopic(ev.target.value)} + rows={3} + disabled={busy} + /> + + + { busy ? _t("Creating...") : _t("Create") }
    ; @@ -164,6 +218,13 @@ const SpaceCreateMenu = ({ onFinished }) => { managed={false} > + { + onFinished(); + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: USER_LABS_TAB, + }); + }} /> { body } ; diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 36ab423885..eb63b21f0e 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useState} from "react"; +import React, { useEffect, useState } from "react"; import classNames from "classnames"; import {Room} from "matrix-js-sdk/src/models/room"; @@ -26,13 +26,11 @@ import {SpaceItem} from "./SpaceTreeLevel"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import SpaceStore, { - HOME_SPACE, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, } from "../../../stores/SpaceStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState"; import NotificationBadge from "../rooms/NotificationBadge"; import { RovingAccessibleButton, @@ -40,13 +38,15 @@ import { RovingTabIndexProvider, } from "../../../accessibility/RovingTabIndex"; import {Key} from "../../../Keyboard"; +import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore"; +import {NotificationState} from "../../../stores/notifications/NotificationState"; interface IButtonProps { space?: Room; className?: string; selected?: boolean; tooltip?: string; - notificationState?: SpaceNotificationState; + notificationState?: NotificationState; isNarrow?: boolean; onClick(): void; } @@ -127,6 +127,12 @@ const SpacePanel = () => { const [invites, spaces, activeSpace] = useSpaces(); const [isPanelCollapsed, setPanelCollapsed] = useState(true); + useEffect(() => { + if (!isPanelCollapsed && menuDisplayed) { + closeMenu(); + } + }, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps + const newClasses = classNames("mx_SpaceButton_new", { mx_SpaceButton_newCancel: menuDisplayed, }); @@ -212,8 +218,8 @@ const SpacePanel = () => { className="mx_SpaceButton_home" onClick={() => SpaceStore.instance.setActiveSpace(null)} selected={!activeSpace} - tooltip={_t("Home")} - notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)} + tooltip={_t("All rooms")} + notificationState={RoomNotificationStateStore.instance.globalState} isNarrow={isPanelCollapsed} /> { invites.map(s => { className={newClasses} tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")} onClick={menuDisplayed ? closeMenu : () => { - openMenu(); if (!isPanelCollapsed) setPanelCollapsed(true); + openMenu(); }} isNarrow={isPanelCollapsed} /> { - setPanelCollapsed(!isPanelCollapsed); - if (menuDisplayed) closeMenu(); - }} + onClick={() => setPanelCollapsed(!isPanelCollapsed)} title={expandCollapseButtonTitle} /> { contextMenu } diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx index fa81b75525..207360c2c2 100644 --- a/src/components/views/spaces/SpacePublicShare.tsx +++ b/src/components/views/spaces/SpacePublicShare.tsx @@ -23,6 +23,7 @@ import {copyPlaintext} from "../../../utils/strings"; import {sleep} from "../../../utils/promise"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {showRoomInviteDialog} from "../../../RoomInvite"; +import {MatrixClientPeg} from "../../../MatrixClientPeg"; interface IProps { space: Room; @@ -50,7 +51,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {

    { _t("Share invite link") }

    { copiedText } - { showRoomInviteDialog(space.roomId); @@ -59,7 +60,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => { >

    { _t("Invite people") }

    { _t("Invite with email or username") } -
    + : null }
    ; }; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index e48e1d5dc2..f34baf256b 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -20,6 +20,7 @@ import {Room} from "matrix-js-sdk/src/models/room"; import RoomAvatar from "../avatars/RoomAvatar"; import SpaceStore from "../../../stores/SpaceStore"; +import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore"; import NotificationBadge from "../rooms/NotificationBadge"; import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton"; import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton"; @@ -68,8 +69,14 @@ export class SpaceItem extends React.PureComponent { constructor(props) { super(props); + const collapsed = SpaceTreeLevelLayoutStore.instance.getSpaceCollapsedState( + props.space.roomId, + this.props.parents, + !props.isNested, // default to collapsed for root items + ); + this.state = { - collapsed: !props.isNested, // default to collapsed for root items + collapsed: collapsed, contextMenuPosition: null, }; } @@ -78,7 +85,14 @@ export class SpaceItem extends React.PureComponent { if (this.props.onExpand && this.state.collapsed) { this.props.onExpand(); } - this.setState({collapsed: !this.state.collapsed}); + const newCollapsedState = !this.state.collapsed; + + SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState( + this.props.space.roomId, + this.props.parents, + newCollapsedState, + ); + this.setState({collapsed: newCollapsedState}); // don't bubble up so encapsulating button for space // doesn't get triggered evt.stopPropagation(); @@ -195,7 +209,7 @@ export class SpaceItem extends React.PureComponent { const userId = this.context.getUserId(); let inviteOption; - if (this.props.space.canInvite(userId)) { + if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) { inviteOption = ( { - // Track percentages to very coarse precision, otherwise 0.002 ends up highlighting a bar. - const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(1)); + // Track percentages to a general precision to avoid over-waking the component. + const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(3)); this.setState({progress}); }; diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx index cdd5bc6641..8c0af5e81a 100644 --- a/src/components/views/voip/DialPadModal.tsx +++ b/src/components/views/voip/DialPadModal.tsx @@ -15,17 +15,14 @@ limitations under the License. */ import * as React from "react"; -import { ensureDMExists } from "../../../createRoom"; import { _t } from "../../../languageHandler"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import AccessibleButton from "../elements/AccessibleButton"; import Field from "../elements/Field"; import DialPad from './DialPad'; import dis from '../../../dispatcher/dispatcher'; -import Modal from "../../../Modal"; -import ErrorDialog from "../../views/dialogs/ErrorDialog"; -import CallHandler from "../../../CallHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload"; +import { Action } from "../../../dispatcher/actions"; interface IProps { onFinished: (boolean) => void; @@ -67,21 +64,11 @@ export default class DialpadModal extends React.PureComponent { } onDialPress = async () => { - const results = await CallHandler.sharedInstance().pstnLookup(this.state.value); - if (!results || results.length === 0 || !results[0].userid) { - Modal.createTrackedDialog('', '', ErrorDialog, { - title: _t("Unable to look up phone number"), - description: _t("There was an error looking up the phone number"), - }); - } - const userId = results[0].userid; - - const roomId = await ensureDMExists(MatrixClientPeg.get(), userId); - - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); + const payload: DialNumberPayload = { + action: Action.DialNumber, + number: this.state.value, + }; + dis.dispatch(payload); this.props.onFinished(true); } diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 30ff74b071..e925f8624b 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -16,8 +16,8 @@ limitations under the License. import { createContext } from "react"; -import {IState} from "../components/structures/RoomView"; -import {Layout} from "../settings/Layout"; +import { IState } from "../components/structures/RoomView"; +import { Layout } from "../settings/Layout"; const RoomContext = createContext({ roomLoading: true, @@ -31,7 +31,6 @@ const RoomContext = createContext({ canPeek: false, showApps: false, isPeeking: false, - showingPinned: false, showReadReceipts: true, showRightPanel: true, joining: false, diff --git a/src/createRoom.ts b/src/createRoom.ts index 310d894266..c6507b1380 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -34,12 +34,13 @@ import { isJoinedOrNearlyJoined } from "./utils/membership"; import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler"; import SpaceStore from "./stores/SpaceStore"; import { makeSpaceParentEvent } from "./utils/space"; +import { Action } from "./dispatcher/actions" // we define a number of interfaces which take their names from the js-sdk /* eslint-disable camelcase */ // TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them -enum Visibility { +export enum Visibility { Public = "public", Private = "private", } @@ -243,7 +244,8 @@ export default function createRoom(opts: IOpts): Promise { // We also failed to join the room (this sets joining to false in RoomViewStore) dis.dispatch({ - action: 'join_room_error', + action: Action.JoinRoomError, + roomId, }); console.error("Failed to create room " + roomId + " " + err); let description = _t("Server may be unavailable, overloaded, or you hit a bug."); diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index cd32c3743f..300eed2b98 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -100,6 +100,12 @@ export enum Action { */ OpenDialPad = "open_dial_pad", + /** + * Dial the phone number in the payload + * payload: DialNumberPayload + */ + DialNumber = "dial_number", + /** * Fired when CallHandler has checked for PSTN protocol support * payload: none @@ -138,4 +144,19 @@ export enum Action { * Fired when an upload is cancelled by the user. Should be used with UploadCanceledPayload. */ UploadCanceled = "upload_canceled", + + /** + * Fired when requesting to join a room + */ + JoinRoom = "join_room", + + /** + * Fired when successfully joining a room + */ + JoinRoomReady = "join_room_ready", + + /** + * Fired when joining a room failed + */ + JoinRoomError = "join_room_error", } diff --git a/src/dispatcher/payloads/DialNumberPayload.ts b/src/dispatcher/payloads/DialNumberPayload.ts new file mode 100644 index 0000000000..1b591b9f6b --- /dev/null +++ b/src/dispatcher/payloads/DialNumberPayload.ts @@ -0,0 +1,23 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface DialNumberPayload extends ActionPayload { + action: Action.DialNumber; + number: string; +} diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 7cf9d9bb9d..cfdb36c7a8 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -116,14 +116,22 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = const parser = new Markdown(md); if (!parser.isPlainText() || forceHTML) { // feed Markdown output to HTML parser - const phtml = cheerio.load(parser.toHTML(), - { _useHtmlParser2: true, decodeEntities: false }); + const phtml = cheerio.load(parser.toHTML(), { + // @ts-ignore: The `_useHtmlParser2` internal option is the + // simplest way to both parse and render using `htmlparser2`. + _useHtmlParser2: true, + decodeEntities: false, + }); if (SettingsStore.getValue("feature_latex_maths")) { // original Markdown without LaTeX replacements const parserOrig = new Markdown(orig); - const phtmlOrig = cheerio.load(parserOrig.toHTML(), - { _useHtmlParser2: true, decodeEntities: false }); + const phtmlOrig = cheerio.load(parserOrig.toHTML(), { + // @ts-ignore: The `_useHtmlParser2` internal option is the + // simplest way to both parse and render using `htmlparser2`. + _useHtmlParser2: true, + decodeEntities: false, + }); // since maths delimiters are handled before Markdown, // code blocks could contain mangled content. diff --git a/src/effects/effect.ts b/src/effects/effect.ts new file mode 100644 index 0000000000..9011b07b61 --- /dev/null +++ b/src/effects/effect.ts @@ -0,0 +1,43 @@ +/* + Copyright 2020 Nurjin Jafar + Copyright 2020 Nordeck IT + Consulting GmbH. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +export type Effect = { + /** + * one or more emojis that will trigger this effect + */ + emojis: Array; + /** + * the matrix message type that will trigger this effect + */ + msgType: string; + /** + * the room command to trigger this effect + */ + command: string; + /** + * a function that returns the translated description of the effect + */ + description: () => string; + /** + * a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message + */ + fallbackMessage: () => string; + /** + * animation options + */ + options: TOptions; +} diff --git a/src/effects/index.ts b/src/effects/index.ts index a22948ebcf..8ecb80020d 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -15,80 +15,11 @@ limitations under the License. */ import { _t, _td } from "../languageHandler"; - -export type Effect = { - /** - * one or more emojis that will trigger this effect - */ - emojis: Array; - /** - * the matrix message type that will trigger this effect - */ - msgType: string; - /** - * the room command to trigger this effect - */ - command: string; - /** - * a function that returns the translated description of the effect - */ - description: () => string; - /** - * a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message - */ - fallbackMessage: () => string; - /** - * animation options - */ - options: TOptions; -} - -type ConfettiOptions = { - /** - * max confetti count - */ - maxCount: number; - /** - * particle animation speed - */ - speed: number; - /** - * the confetti animation frame interval in milliseconds - */ - frameInterval: number; - /** - * the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible) - */ - alpha: number; - /** - * use gradient instead of solid particle color - */ - gradient: boolean; -}; -type FireworksOptions = { - /** - * max fireworks count - */ - maxCount: number; - /** - * gravity value that firework adds to shift from it's start position - */ - gravity: number; -} -type SnowfallOptions = { - /** - * The maximum number of snowflakes to render at a given time - */ - maxCount: number; - /** - * The amount of gravity to apply to the snowflakes - */ - gravity: number; - /** - * The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies. - */ - maxDrift: number; -} +import { ConfettiOptions } from "./confetti"; +import { Effect } from "./effect"; +import { FireworksOptions } from "./fireworks"; +import { SnowfallOptions } from "./snowfall"; +import { SpaceInvadersOptions } from "./spaceinvaders"; /** * This configuration defines room effects that can be triggered by custom message types and emojis @@ -131,6 +62,17 @@ export const CHAT_EFFECTS: Array> = [ maxDrift: 5, }, } as Effect, + { + emojis: ["👾", "🌌"], + msgType: "io.element.effects.space_invaders", + command: "spaceinvaders", + description: () => _td("Sends the given message with a space themed effect"), + fallbackMessage: () => _t("sends space invaders") + " 👾", + options: { + maxCount: 50, + gravity: 0.01, + }, + } as Effect, ]; diff --git a/src/effects/spaceinvaders/index.ts b/src/effects/spaceinvaders/index.ts new file mode 100644 index 0000000000..9520e8366a --- /dev/null +++ b/src/effects/spaceinvaders/index.ts @@ -0,0 +1,119 @@ +/* + Copyright 2021 The Matrix.org Foundation C.I.C. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import ICanvasEffect from '../ICanvasEffect'; +import { arrayFastClone } from "../../utils/arrays"; + +export type SpaceInvadersOptions = { + /** + * The maximum number of invaders to render at a given time + */ + maxCount: number; + /** + * The amount of gravity to apply to the invaders + */ + gravity: number; +} + +type Invader = { + x: number; + y: number; + xCol: number; + gravity: number; +} + +export const DefaultOptions: SpaceInvadersOptions = { + maxCount: 50, + gravity: 0.005, +}; + +const KEY_FRAME_INTERVAL = 15; // 15ms, roughly +const GLYPH = "👾"; + +export default class SpaceInvaders implements ICanvasEffect { + private readonly options: SpaceInvadersOptions; + + constructor(options: { [key: string]: any }) { + this.options = {...DefaultOptions, ...options}; + } + + private context: CanvasRenderingContext2D | null = null; + private particles: Array = []; + private lastAnimationTime: number; + + public isRunning: boolean; + + public start = async (canvas: HTMLCanvasElement, timeout = 3000) => { + if (!canvas) { + return; + } + this.context = canvas.getContext('2d'); + this.particles = []; + const count = this.options.maxCount; + while (this.particles.length < count) { + this.particles.push(this.resetParticle({} as Invader, canvas.width, canvas.height)); + } + this.isRunning = true; + requestAnimationFrame(this.renderLoop); + if (timeout) { + window.setTimeout(this.stop, timeout); + } + } + + public stop = async () => { + this.isRunning = false; + } + + private resetParticle = (particle: Invader, width: number, height: number): Invader => { + particle.x = Math.random() * width; + particle.y = Math.random() * -height; + particle.xCol = particle.x; + particle.gravity = this.options.gravity + (Math.random() * 6) + 4; + return particle; + } + + private renderLoop = (): void => { + if (!this.context || !this.context.canvas) { + return; + } + if (this.particles.length === 0) { + this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); + } else { + const timeDelta = Date.now() - this.lastAnimationTime; + if (timeDelta >= KEY_FRAME_INTERVAL || !this.lastAnimationTime) { + // Clear the screen first + this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); + + this.lastAnimationTime = Date.now(); + this.animateAndRenderInvaders(); + } + requestAnimationFrame(this.renderLoop); + } + }; + + private animateAndRenderInvaders() { + if (!this.context || !this.context.canvas) { + return; + } + this.context.font = "50px Twemoji"; + for (const particle of arrayFastClone(this.particles)) { + particle.y += particle.gravity; + + this.context.save(); + this.context.fillText(GLYPH, particle.x, particle.y); + this.context.restore(); + } + } +} diff --git a/src/hooks/useAsyncMemo.ts b/src/hooks/useAsyncMemo.ts index 38c70de259..1776ec1d36 100644 --- a/src/hooks/useAsyncMemo.ts +++ b/src/hooks/useAsyncMemo.ts @@ -14,14 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {useState, useEffect, DependencyList} from 'react'; +import { useState, useEffect, DependencyList } from 'react'; type Fn = () => Promise; export const useAsyncMemo = (fn: Fn, deps: DependencyList, initialValue?: T): T => { const [value, setValue] = useState(initialValue); useEffect(() => { - fn().then(setValue); + let discard = false; + fn().then(v => { + if (!discard) { + setValue(v); + } + }); + return () => { + discard = true; + }; }, deps); // eslint-disable-line react-hooks/exhaustive-deps return value; }; diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 8c6ea60a8d..294d5a4979 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -728,7 +728,7 @@ "Enable them now": "Включете ги сега", "Toolbox": "Инструменти", "Collecting logs": "Събиране на логове", - "You must specify an event type!": "Трябва да укажате тип на събитието", + "You must specify an event type!": "Трябва да укажате тип на събитието!", "(HTTP status %(httpStatus)s)": "(HTTP статус %(httpStatus)s)", "All Rooms": "Във всички стаи", "Wednesday": "Сряда", @@ -2831,5 +2831,71 @@ "Åland Islands": "Оландски острови", "Afghanistan": "Афганистан", "United States": "Съединените щати", - "United Kingdom": "Обединеното кралство" + "United Kingdom": "Обединеното кралство", + "Manage & explore rooms": "Управление и откриване на стаи", + "Space options": "Опции на пространството", + "Workspace: ": "Работна област: ", + "Channel: ": "Канал: ", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Пространствата са нов начин за групиране на стаи и хора. За да се присъедините към съществуващо пространство, се нуждаете от покана.", + "Open space for anyone, best for communities": "Открийте пространство за всеки, най-добро за общности", + "Add existing room": "Добави съществуваща стая", + "Leave space": "Напусни пространство", + "Invite with email or username": "Покани чрез имейл или потребителско име", + "Invite people": "Покани хора", + "Share invite link": "Сподели връзка с покана", + "Click to copy": "Натиснете за копиране", + "Delete": "Изтрий", + "Please enter a name for the space": "Моля, въведете име на пространството", + "Create a space": "Създаване на пространство", + "Add some details to help people recognise it.": "Добавете някои подробности, за да помогнете на хората да го разпознаят.", + "Invite only, best for yourself or teams": "Само с покана, най-добро за вас самият или отбори", + "Public": "Публично", + "Private": "Лично", + "You can change this later": "Можете да го промените по-късно", + "Your public space": "Вашето публично пространство", + "Your private space": "Вашето лично пространство", + "You can change these anytime.": "Можете да ги промените по всяко време.", + "Creating...": "Създаване...", + "Collapse space panel": "Свий панел с пространства", + "Expand space panel": "Разшири панел с пространства", + "unknown person": "", + "sends snowfall": "изпраща снеговалеж", + "Sends the given message with snowfall": "Изпраща даденото съобщение със снеговалеж", + "Sends the given message with fireworks": "Изпраща даденото съобщение с фойерверки", + "sends fireworks": "изпраща фойерверки", + "sends confetti": "изпраща конфети", + "Sends the given message with confetti": "Изпраща даденото съобщение с конфети", + "Show chat effects (animations when receiving e.g. confetti)": "Покажи чат ефектите (анимации при получаване, като например конфети)", + "Use Ctrl + Enter to send a message": "Използвай Ctrl + Enter за изпращане на съобщение", + "Use Command + Enter to send a message": "Използвай Command + Enter за изпращане на съобщение", + "Use Ctrl + F to search": "Използвай Ctrl + F за търсене", + "Use Command + F to search": "Използвай Command + F за търсене", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Вашата обратна връзка ще направи пространствата по-добри. Kолкото повече изпаднете в подобробности, толкова по-добре.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Бета версията е налична за уеб, десктоп и Android. Някои функции може да не са налични на вашия Home сървър.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Можете да напуснете бета версията по всяко време от настройките или чрез докосване на симовола за бета версията, като този по-горе.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s ще се презареди с пуснати Пространства. Общностите и собствените етикети ще бъдат скрити.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Ако напуснете, %(brand)s ще се презареди със изключени Пространства. Общностите и собствените етикети ще бъдат видими отново.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Бета версията е налична за уеб, десктоп и Android. Благодарим ви, че изпробвахте бета версията.", + "Spaces are a new way to group rooms and people.": "Пространствата са нов начин за групиране на стаи и хора.", + "Spaces": "Пространства", + "Check your devices": "Проверете устройствата си", + "%(deviceId)s from %(ip)s": "%(deviceId)s от %(ip)s", + "This homeserver has been blocked by it's administrator.": "Този Home сървър е бил блокиран от администратора му.", + "Use app for a better experience": "Използвайте приложението за по-добра работа", + "Use app": "Използване на приложението", + "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web е експериментален на мобилни устройства. За по-добра работа и най-новите функции, използвайте нашето безплатно приложение.", + "Review to ensure your account is safe": "Прегледайте, за да уверите, че профилът ви е в безопастност", + "You have unverified logins": "Имате неверифицирани сесии", + "Share your public space": "Споделете публичното си място", + "Invite to %(spaceName)s": "Покани в %(spaceName)s", + "%(senderName)s has updated the widget layout": "%(senderName)s обнови оформлението на приспособлението", + "Sends the given message as a spoiler": "Изпраща даденото съобщение като спойлер", + "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Вашият Home сървър отхвърли вашия опит за влизане. Това може да се дължи на неща, които просто отнемат твърде много време. Моля, опитайте отново. Ако това продължи, моля, свържете се със админитратора на вашия Home сървър.", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Вашият Home сървър беше недостижим и не можа да ви впише. Моля, опитайте отново. Ако това продължи, моля, свържете се със админитратора на вашия Home сървър.", + "Try again": "Опитайте отново", + "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Помолихме браузъра да запомни кой Home сървър използвате за влизане, но за съжаление браузърът ви го е забравил. Отидете на страницата за влизане и опитайте отново.", + "Already in call": "Вече в разговор", + "You're already in a call with this person.": "Вече сте в разговор в този човек.", + "Too Many Calls": "Твърде много повиквания", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Неуспешно повикване поради неуспешен достъп до микрофон. Проверете дали микрофонът е включен и настроен правилно." } diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 8633146420..75472b4d38 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -1187,7 +1187,7 @@ "Changes your display nickname in the current room only": "Změní vaši zobrazovanou přezdívku pouze v této místnosti", "User %(userId)s is already in the room": "Uživatel %(userId)s už je v této místnosti", "The user must be unbanned before they can be invited.": "Uživatel je vykázán, nelze ho pozvat.", - "Show read receipts sent by other users": "Zobrazovat potvrzení o přijetí", + "Show read receipts sent by other users": "Zobrazovat potvrzení o přečtení", "Scissors": "Nůžky", "Accept all %(invitedRooms)s invites": "Přijmout pozvání do všech těchto místností: %(invitedRooms)s", "Change room avatar": "Změnit avatar místnosti", @@ -3155,7 +3155,7 @@ "Invite People": "Pozvat lidi", "Invite with email or username": "Pozvěte e-mailem nebo uživatelským jménem", "You can change these anytime.": "Tyto údaje můžete kdykoli změnit.", - "Add some details to help people recognise it.": "Přidejte několik podrobností, aby to lidé lépe rozpoznali.", + "Add some details to help people recognise it.": "Přidejte nějaké podrobnosti, aby ho lidé lépe rozpoznali.", "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Prostory jsou nový způsob, jak seskupovat místnosti a lidi. Chcete-li se připojit ke stávajícímu prostoru, budete potřebovat pozvánku.", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "K vašemu účtu přistupuje nové přihlášení: %(name)s (%(deviceID)s) pomocí %(ip)s", "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Z %(deviceName)s (%(deviceId)s) pomocí %(ip)s", @@ -3201,7 +3201,7 @@ "Please choose a strong password": "Vyberte silné heslo", "What are some things you want to discuss in %(spaceName)s?": "O kterých tématech chcete diskutovat v %(spaceName)s?", "Let's create a room for each of them.": "Vytvořme pro každé z nich místnost.", - "Use another login": "Použijte jiné přihlašovací jméno", + "Use another login": "Použít jinou relaci", "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Bez ověření nebudete mít přístup ke všem svým zprávám a ostatním se můžete zobrazit jako nedůvěryhodný.", "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Jste zde jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás.", "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Pokud vše resetujete, začnete bez důvěryhodných relací, bez důvěryhodných uživatelů a možná nebudete moci zobrazit minulé zprávy.", @@ -3225,5 +3225,72 @@ "Including %(commaSeparatedMembers)s": "Včetně %(commaSeparatedMembers)s", "View all %(count)s members|one": "Zobrazit jednoho člena", "View all %(count)s members|other": "Zobrazit všech %(count)s členů", - "Failed to send": "Odeslání se nezdařilo" + "Failed to send": "Odeslání se nezdařilo", + "What do you want to organise?": "Co si přejete organizovat?", + "Filter all spaces": "Filtrovat všechny prostory", + "Delete recording": "Smazat zvukovou zprávu", + "Stop the recording": "Zastavit nahrávání", + "%(count)s results in all spaces|one": "%(count)s výsledek ve všech prostorech", + "%(count)s results in all spaces|other": "%(count)s výsledků ve všech prostorech", + "Play": "Přehrát", + "Pause": "Pozastavit", + "Enter your Security Phrase a second time to confirm it.": "Zadejte bezpečnostní frázi podruhé a potvrďte ji.", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Vyberte místnosti nebo konverzace, které chcete přidat. Toto je prostor pouze pro vás, nikdo nebude informován. Později můžete přidat další.", + "You have no ignored users.": "Nemáte žádné ignorované uživatele.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Jedná se o experimentální funkci. Noví uživatelé, kteří obdrží pozvánku, ji budou muset otevřít na , aby se mohli připojit.", + "To join %(spaceName)s, turn on the
    Spaces beta": "Pro připojení k %(spaceName)s, zapněte Prostory beta", + "To view %(spaceName)s, turn on the Spaces beta": "Pro zobrazení %(spaceName)s, zapněte Prostory beta", + "Select a room below first": "Nejprve si vyberte místnost níže", + "Communities are changing to Spaces": "Skupiny se mění na Prostory", + "Join the beta": "Připojit se k beta verzi", + "Leave the beta": "Opustit beta verzi", + "Beta": "Beta", + "Tap for more info": "Klepněte pro více informací", + "Spaces is a beta feature": "Prostory jsou beta verze", + "Want to add a new room instead?": "Chcete místo toho přidat novou místnost?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Přidávání místnosti...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Přidávání místností... (%(progress)s z %(count)s)", + "Not all selected were added": "Ne všechny vybrané byly přidány", + "You can add existing spaces to a space.": "Do prostoru můžete přidat existující prostory.", + "Feeling experimental?": "Chcete experimentovat?", + "You are not allowed to view this server's rooms list": "Namáte oprávnění zobrazit seznam místností tohoto serveru", + "Error processing voice message": "Chyba při zpracování hlasové zprávy", + "We didn't find a microphone on your device. Please check your settings and try again.": "Ve vašem zařízení nebyl nalezen žádný mikrofon. Zkontrolujte prosím nastavení a zkuste to znovu.", + "No microphone found": "Nebyl nalezen žádný mikrofon", + "We were unable to access your microphone. Please check your browser settings and try again.": "Nepodařilo se získat přístup k vašemu mikrofonu . Zkontrolujte prosím nastavení prohlížeče a zkuste to znovu.", + "Unable to access your microphone": "Nelze získat přístup k mikrofonu", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Chcete experimentovat? Laboratoře jsou nejlepším způsobem, jak získat novinky v raném stádiu, vyzkoušet nové funkce a pomoci je formovat ještě před jejich spuštěním. Zjistěte více.", + "Your access token gives full access to your account. Do not share it with anyone.": "Přístupový token vám umožní plný přístup k účtu. Nikomu ho nesdělujte.", + "Access Token": "Přístupový token", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Prostory představují nový způsob seskupování místností a osob. Chcete-li se připojit k existujícímu prostoru, potřebujete pozvánku.", + "Please enter a name for the space": "Zadejte prosím název prostoru", + "Connecting": "Spojování", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Povolit Peer-to-Peer pro hovory 1:1 (pokud tuto funkci povolíte, druhá strana může vidět vaši IP adresu)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta verze je k dispozici pro web, desktop a Android. Některé funkce mohou být na vašem domovském serveru nedostupné.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Beta verzi můžete kdykoli opustit v nastavení nebo klepnutím na štítek beta verze, jako je ten výše.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s se znovu načte s povolenými Prostory. Skupiny a vlastní značky budou skryty.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta verze je k dispozici pro web, desktop a Android. Děkujeme vám za vyzkoušení beta verze.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s se znovu načte s vypnutými Prostory. Skupiny a vlastní značky budou opět viditelné.", + "Spaces are a new way to group rooms and people.": "Prostory představují nový způsob seskupování místností a osob.", + "Message search initialisation failed": "Inicializace vyhledávání zpráv se nezdařila", + "Spaces are a beta feature.": "Prostory jsou funkcí beta verze.", + "Search names and descriptions": "Hledat názvy a popisy", + "You may contact me if you have any follow up questions": "V případě dalších dotazů se na mě můžete obrátit", + "To leave the beta, visit your settings.": "Chcete-li opustit beta verzi, jděte do nastavení.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Vaše platforma a uživatelské jméno budou zaznamenány, abychom mohli co nejlépe využít vaši zpětnou vazbu.", + "%(featureName)s beta feedback": "%(featureName)s zpětná vazba beta verze", + "Thank you for your feedback, we really appreciate it.": "Děkujeme za vaši zpětnou vazbu, velmi si jí vážíme.", + "Beta feedback": "Zpětná vazba na betaverzi", + "Add reaction": "Přidat reakci", + "Send and receive voice messages": "Odeslat a přijmout hlasové zprávy", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Vaše zpětná vazba pomůže zlepšit prostory. Čím podrobnější bude, tím lépe.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Pokud odejdete, %(brand)s se znovu načte s vypnutými Prostory. Skupiny a vlastní značky budou opět viditelné.", + "Space Autocomplete": "Automatické dokončení prostoru", + "Go to my space": "Přejít do mého prostoru", + "sends space invaders": "pošle space invaders", + "Sends the given message with a space themed effect": "Odešle zadanou zprávu s efektem vesmíru", + "See when people join, leave, or are invited to your active room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do vaší aktivní místnosti", + "Kick, ban, or invite people to this room, and make you leave": "Vykopnout, vykázat, pozvat lidi do této místnosti nebo odejít", + "Kick, ban, or invite people to your active room, and make you leave": "Vykopnout, vykázat, pozvat lidi do vaší aktivní místnosti nebo odejít", + "See when people join, leave, or are invited to this room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do této místnosti" } diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 7f0d6a2747..dcc5343af4 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -88,7 +88,7 @@ "Unban": "Verbannung aufheben", "unknown error code": "Unbekannter Fehlercode", "Upload avatar": "Profilbild hochladen", - "Upload file": "Datei hochladen", + "Upload file": "Datei senden", "Users": "Benutzer", "Verification Pending": "Verifizierung ausstehend", "Video call": "Videoanruf", @@ -114,7 +114,7 @@ "VoIP is unsupported": "VoIP wird nicht unterstützt", "You are already in a call.": "Du bist bereits in einem Gespräch.", "You cannot place a call with yourself.": "Du kannst keinen Anruf mit dir selbst starten.", - "You cannot place VoIP calls in this browser.": "VoIP-Gespräche werden von diesem Browser nicht unterstützt.", + "You cannot place VoIP calls in this browser.": "Anrufe werden von diesem Browser nicht unterstützt.", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Heimserver verbunden zu sein.", "Sun": "So", "Mon": "Mo", @@ -338,7 +338,7 @@ "Uploading %(filename)s and %(count)s others|one": "%(filename)s und %(count)s weitere Dateien werden hochgeladen", "Uploading %(filename)s and %(count)s others|other": "%(filename)s und %(count)s weitere Dateien werden hochgeladen", "You must register to use this functionality": "Du musst dich registrieren, um diese Funktionalität nutzen zu können", - "Create new room": "Neuen Raum erstellen", + "Create new room": "Neuer Raum", "Room directory": "Raum-Verzeichnis", "Start chat": "Chat starten", "New Password": "Neues Passwort", @@ -451,7 +451,7 @@ "Pinned Messages": "Angeheftete Nachrichten", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s hat die angehefteten Nachrichten für diesen Raum geändert.", "Jump to read receipt": "Zur Lesebestätigung springen", - "Message Pinning": "Nachrichtenanheftung", + "Message Pinning": "Nachrichten anheften", "Long Description (HTML)": "Lange Beschreibung (HTML)", "Jump to message": "Zur Nachricht springen", "No pinned messages.": "Keine angehefteten Nachrichten vorhanden.", @@ -795,7 +795,7 @@ "We encountered an error trying to restore your previous session.": "Wir haben ein Problem beim Wiederherstellen deiner vorherigen Sitzung festgestellt.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Den Browser-Speicher zu löschen kann das Problem lösen, wird dich aber abmelden und verschlüsselte Chats unlesbar machen.", "Collapse Reply Thread": "Antwort-Thread zusammenklappen", - "Enable widget screenshots on supported widgets": "Bildschirmfotos bei unterstützten Widgets aktivieren", + "Enable widget screenshots on supported widgets": "Bildschirmfotos für unterstützte Widgets", "Send analytics data": "Analysedaten senden", "e.g. %(exampleValue)s": "z.B. %(exampleValue)s", "Muted Users": "Stummgeschaltete Benutzer", @@ -980,7 +980,7 @@ "Enable Emoji suggestions while typing": "Emojivorschläge während Eingabe", "Show a placeholder for removed messages": "Platzhalter für gelöschte Nachrichten", "Show join/leave messages (invites/kicks/bans unaffected)": "Betreten oder Verlassen von Benutzern (ausgen. Einladungen/Rauswürfe/Banne)", - "Show avatar changes": "Avataränderungen anzeigen", + "Show avatar changes": "Avataränderungen", "Show display name changes": "Änderungen von Anzeigenamen", "Send typing notifications": "Tippbenachrichtigungen senden", "Show avatars in user and room mentions": "Avatare in Benutzer- und Raumerwähnungen", @@ -1017,7 +1017,7 @@ "Language and region": "Sprache und Region", "Theme": "Design", "Account management": "Benutzerkontenverwaltung", - "For help with using %(brand)s, click here.": "Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier.", + "For help with using %(brand)s, click here.": "Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier oder beginne einen Chat mit unserem Bot. Klicke dazu auf den unteren Knopf.", "Chat with %(brand)s Bot": "Chatte mit dem %(brand)s-Bot", "Help & About": "Hilfe und Über", @@ -1200,11 +1200,11 @@ "Scissors": "Schere", "Upgrade to your own domain": "Upgrade zu deiner eigenen Domain", "Accept all %(invitedRooms)s invites": "Akzeptiere alle %(invitedRooms)s Einladungen", - "Change room avatar": "Ändere Raumbild", - "Change room name": "Ändere Raumname", - "Change main address for the room": "Ändere Hauptadresse für den Raum", + "Change room avatar": "Raumbild ändern", + "Change room name": "Raumname ändern", + "Change main address for the room": "Hauptadresse ändern", "Change history visibility": "Sichtbarkeit des Verlaufs ändern", - "Change permissions": "Ändere Berechtigungen", + "Change permissions": "Berechtigungen ändern", "Change topic": "Thema ändern", "Modify widgets": "Widgets bearbeiten", "Default role": "Standard-Rolle", @@ -1236,7 +1236,7 @@ "Name or Matrix ID": "Name oder Matrix-ID", "Your %(brand)s is misconfigured": "Dein %(brand)s ist falsch konfiguriert", "You cannot modify widgets in this room.": "Du darfst in diesem Raum keine Widgets verändern.", - "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Ob du die \"Breadcrumbs\"-Funktion nutzt oder nicht (Avatare oberhalb der Raumliste)", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Ob du die Liste der kürzlich besuchten Räume oberhalb der Raumliste nutzt", "The server does not support the room version specified.": "Der Server unterstützt die angegebene Raumversion nicht.", "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.": "Achtung: Ein Raum-Upgrade wird die Mitglieder des Raumes nicht automatisch auf die neue Version migrieren. Wir werden in der alten Raumversion einen Link zum neuen Raum posten - Raummitglieder müssen dann auf diesen Link klicken um dem neuen Raum beizutreten.", "Replying With Files": "Mit Dateien antworten", @@ -1336,7 +1336,7 @@ "Sends a message as plain text, without interpreting it as markdown": "Verschickt eine Nachricht in Rohtext, ohne sie als Markdown darzustellen", "Use an identity server to invite by email. Manage in Settings.": "Mit einem Identitätsserver kannst du über E-Mail Einladungen zu verschicken. Verwalte ihn in den Einstellungen.", "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", - "Try out new ways to ignore people (experimental)": "Verwende neue Möglichkeiten, Menschen zu blockieren (experimentell)", + "Try out new ways to ignore people (experimental)": "Verwende neue Möglichkeiten, Menschen zu blockieren", "Send read receipts for messages (requires compatible homeserver to disable)": "Lesebestätigungen für Nachrichten senden (Deaktivieren erfordert einen kompatiblen Heimserver)", "My Ban List": "Meine Bannliste", "This is your list of users/servers you have blocked - don't leave the room!": "Dies ist die Liste von Benutzer und Servern, die du blockiert hast - verlasse diesen Raum nicht!", @@ -1368,7 +1368,7 @@ "%(num)s hours from now": "in %(num)s Stunden", "about a day from now": "in etwa einem Tag", "%(num)s days from now": "in %(num)s Tagen", - "Show info about bridges in room settings": "Information über Brücken in den Raumeinstellungen anzeigen", + "Show info about bridges in room settings": "Information über Brücken in Raumeinstellungen", "Enable message search in encrypted rooms": "Nachrichtensuche in verschlüsselten Räumen aktivieren", "Lock": "Schloss", "Later": "Später", @@ -1634,7 +1634,7 @@ "Show rooms with unread notifications first": "Räume mit ungelesenen Benachrichtigungen zuerst zeigen", "Show shortcuts to recently viewed rooms above the room list": "Kürzlich besuchte Räume anzeigen", "Use Single Sign On to continue": "Einmalanmeldung zum Fortfahren nutzen", - "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte E-Mail-Adresse mit der Einmalanmeldung, um deine Identität nachzuweisen.", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige das Hinzufügen dieser E-Mail-Adresse durch Single Sign-on, um deine Identität nachzuweisen.", "Single Sign On": "Einmalanmeldung", "Confirm adding email": "Hinzugefügte E-Mail-Addresse bestätigen", "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte Telefonnummer, indem du deine Identität mittels der Einmalanmeldung nachweist.", @@ -1651,7 +1651,7 @@ "Not Trusted": "Nicht vertraut", "Manually Verify by Text": "Verifiziere manuell mit einem Text", "Interactively verify by Emoji": "Verifiziere interaktiv mit Emojis", - "Support adding custom themes": "Unterstütze das Hinzufügen von benutzerdefinierten Designs", + "Support adding custom themes": "Benutzerdefinierte Designs", "Ask this user to verify their session, or manually verify it below.": "Bitte diesen Nutzer, seine Sitzung zu verifizieren, oder verifiziere diese unten manuell.", "a few seconds from now": "in ein paar Sekunden", "Manually verify all remote sessions": "Remotesitzungen manuell verifizieren", @@ -1700,7 +1700,7 @@ "This room is end-to-end encrypted": "Dieser Raum ist Ende-zu-Ende verschlüsselt", "You are not subscribed to any lists": "Du hast keine Listen abonniert", "Error adding ignored user/server": "Fehler beim Blockieren eines Nutzers/Servers", - "None": "Keine", + "None": "Nichts", "Ban list rules - %(roomName)s": "Verbotslistenregeln - %(roomName)s", "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.": "Füge hier die Benutzer und Server hinzu, die du blockieren willst. Verwende Sternchen, damit %(brand)s mit beliebigen Zeichen übereinstimmt. Bspw. würde @bot: * alle Benutzer blockieren, die auf einem Server den Namen 'bot' haben.", "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.": "Das Ignorieren von Personen erfolgt über Sperrlisten. Wenn eine Sperrliste abonniert wird, werden die von dieser Liste blockierten Benutzer und Server ausgeblendet.", @@ -1764,7 +1764,7 @@ "Backup has a valid signature from unverified session ": "Die Sicherung hat eine gültige Signatur von einer nicht verifizierten Sitzung ", "Backup has an invalid signature from verified session ": "Die Sicherung hat eine ungültige Signatur von einer verifizierten Sitzung ", "Backup has an invalid signature from unverified session ": "Die Sicherung hat eine ungültige Signatur von einer nicht verifizierten Sitzung ", - "Your keys are not being backed up from this session.": "Deine Schlüssel werden nicht von dieser Sitzung gesichert.", + "Your keys are not being backed up from this session.": "Deine Schlüssel werden von dieser Sitzung nicht gesichert.", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Zur Zeit verwendest du , um Kontakte zu finden und von anderen gefunden zu werden. Du kannst deinen Identitätsserver weiter unten ändern.", "Invalid theme schema.": "Ungültiges Designschema.", "Error downloading theme information.": "Fehler beim herunterladen des Themas.", @@ -2156,7 +2156,7 @@ "Liberate your communication": "Befreie deine Kommunikation", "Message downloading sleep time(ms)": "Wartezeit zwischen dem Herunterladen von Nachrichten (ms)", "Navigate recent messages to edit": "Letzte Nachrichten zur Bearbeitung ansehen", - "Jump to start/end of the composer": "Springe zum Anfang/Ende der Nachrichteneingabe", + "Jump to start/end of the composer": "Zu Anfang/Ende des Textfelds springen", "Navigate composer history": "Verlauf der Nachrichteneingabe durchsuchen", "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "Wenn du dies versehentlich getan hast, kannst du in dieser Sitzung \"sichere Nachrichten\" einrichten, die den Nachrichtenverlauf dieser Sitzung mit einer neuen Wiederherstellungsmethode erneut verschlüsseln.", "Cancel replying to a message": "Nachricht beantworten abbrechen", @@ -2321,7 +2321,7 @@ "%(senderName)s invited %(targetName)s": "%(senderName)s hat %(targetName)s eingeladen", "You changed the room topic": "Du hast das Raumthema geändert", "%(senderName)s changed the room topic": "%(senderName)s hat das Raumthema geändert", - "New spinner design": "Neue Warteanimation", + "New spinner design": "Neue Ladeanimation", "Use a more compact ‘Modern’ layout": "Modernes kompaktes Layout", "Message deleted on %(date)s": "Nachricht am %(date)s gelöscht", "Wrong file type": "Falscher Dateityp", @@ -2337,7 +2337,7 @@ "Are you sure you want to cancel entering passphrase?": "Bist du sicher, dass du die Eingabe der Passphrase abbrechen möchtest?", "Use your account to sign in to the latest version": "Melde dich mit deinem Account in der neuesten Version an", "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s", - "Enable advanced debugging for the room list": "Erweiterte Fehlersuche für die Raumliste aktivieren", + "Enable advanced debugging for the room list": "Erweiterte Fehlersuche für die Raumliste", "Enable experimental, compact IRC style layout": "Kompaktes Layout im IRC-Stil (experimentell)", "User menu": "Benutzermenü", "%(brand)s Web": "%(brand)s Web", @@ -2345,10 +2345,10 @@ "%(brand)s iOS": "%(brand)s iOS", "%(brand)s X for Android": "%(brand)s X für Android", "We’re excited to announce Riot is now Element": "Wir freuen uns zu verkünden, dass Riot jetzt Element ist", - "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s kann verschlüsselte Nachrichten nicht sicher während der Ausführung im Browser durchsuchen. Benutze %(brand)s Desktop, um verschlüsselte Nachrichten in den Suchergebnissen angezeigt zu bekommen.", + "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "Das Durchsuchen von verschlüsselten Nachrichten wird aus Sicherheitsgründen nur von %(brand)s Desktop unterstützt. Hier geht's zum Download.", "Show rooms with unread messages first": "Räume mit ungelesenen Nachrichten zuerst zeigen", "Show previews of messages": "Nachrichtenvorschau anzeigen", - "Use default": "Standardeinstellungen benutzen", + "Use default": "Standardeinstellungen", "Mentions & Keywords": "Erwähnungen und Schlüsselwörter", "Notification options": "Benachrichtigungsoptionen", "Forget Room": "Raum vergessen", @@ -2378,7 +2378,7 @@ "A connection error occurred while trying to contact the server.": "Beim Versuch, den Server zu kontaktieren, ist ein Verbindungsfehler aufgetreten.", "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Du hast sie ggf. in einem anderen Client als %(brand)s konfiguriert. Du kannst sie nicht in %(brand)s verändern, aber sie werden trotzdem angewandt.", "Master private key:": "Privater Hauptschlüssel:", - "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart & %(brand)s wird versuchen, sie zu verwenden.", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart und %(brand)s wird versuchen, sie zu verwenden.", "Custom Tag": "Benutzerdefinierter Tag", "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at element.io/get-started.": "Du bist bereits eingeloggt und kannst loslegen. Allerdings kannst du auch die neuesten Versionen der App für alle Plattformen unter element.io/get-started herunterladen.", "You're all caught up.": "Alles gesichtet.", @@ -2488,7 +2488,7 @@ "Secret storage:": "Sicherer Speicher:", "ready": "bereit", "not ready": "nicht bereit", - "Secure Backup": "Sichere Aufbewahrungskopie", + "Secure Backup": "Sicheres Backup", "End Call": "Anruf beenden", "Remove the group call from the room?": "Konferenzgespräch aus diesem Raum entfernen?", "You don't have permission to remove the call from the room": "Du hast keine Berechtigung um den Konferenzanruf aus dem Raum zu entfernen", @@ -2521,7 +2521,7 @@ "Ignored attempt to disable encryption": "Versuch, die Verschlüsselung zu deaktivieren, wurde ignoriert", "Failed to save your profile": "Speichern des Profils fehlgeschlagen", "The operation could not be completed": "Die Operation konnte nicht abgeschlossen werden", - "Remove messages sent by others": "Nachrichten von anderen entfernen", + "Remove messages sent by others": "Nachrichten von anderen löschen", "Starting camera...": "Starte Kamera...", "Call connecting...": "Verbinde den Anruf...", "Calling...": "Rufe an...", @@ -2597,7 +2597,7 @@ "See videos posted to your active room": "In deinen aktiven Raum gesendete Videos anzeigen", "See videos posted to this room": "In diesen Raum gesendete Videos anzeigen", "Send images as you in this room": "Bilder als du in diesen Raum senden", - "Send images as you in your active room": "Sende Bilder als deine Person in den aktiven Raum.", + "Send images as you in your active room": "Sende Bilder in den aktuellen Raum", "See images posted to this room": "In diesen Raum gesendete Bilder anzeigen", "See images posted to your active room": "In deinen aktiven Raum gesendete Bilder anzeigen", "Send videos as you in this room": "Videos als du in diesen Raum senden", @@ -2699,21 +2699,21 @@ "Switzerland": "Schweiz", "Sweden": "Schweden", "Swaziland": "Swasiland", - "Svalbard & Jan Mayen": "Spitzbergen & Jan Mayen", + "Svalbard & Jan Mayen": "Spitzbergen und Jan Mayen", "Suriname": "Surinam", "Sudan": "Sudan", "St. Vincent & Grenadines": "St. Vincent und die Grenadinen", - "St. Pierre & Miquelon": "St. Pierre & Miquelon", + "St. Pierre & Miquelon": "St. Pierre und Miquelon", "St. Martin": "St. Martin", "St. Lucia": "St. Lucia", - "St. Kitts & Nevis": "St. Kitts & Nevis", + "St. Kitts & Nevis": "St. Kitts und Nevis", "St. Helena": "St. Helena", "St. Barthélemy": "St. Barthélemy", "Sri Lanka": "Sri Lanka", "Spain": "Spanien", "South Sudan": "Südsudan", "South Korea": "Südkorea", - "South Georgia & South Sandwich Islands": "Südgeorgien & Südliche Sandwichinseln", + "South Georgia & South Sandwich Islands": "Südgeorgien und Südliche Sandwichinseln", "South Africa": "Südafrika", "Somalia": "Somalia", "Solomon Islands": "Salomonen", @@ -2815,7 +2815,7 @@ "Hungary": "Ungarn", "Hong Kong": "Hongkong", "Honduras": "Honduras", - "Heard & McDonald Islands": "Heard & McDonald-Inseln", + "Heard & McDonald Islands": "Heard und McDonald-Inseln", "Haiti": "Haiti", "Guyana": "Guyana", "Guinea-Bissau": "Guinea-Bissau", @@ -2916,7 +2916,7 @@ "United Kingdom": "Großbritannien", "We call the places you where you can host your account ‘homeservers’.": "Orte, an denen du dein Benutzerkonto hosten kannst, nennen wir \"Homeserver\".", "Specify a homeserver": "Gib einen Homeserver an", - "Render LaTeX maths in messages": "LaTeX-Matheformeln in Nachrichten anzeigen", + "Render LaTeX maths in messages": "LaTeX-Matheformeln", "Decide where your account is hosted": "Gib an wo dein Benutzerkonto gehostet werden soll", "Already have an account? Sign in here": "Hast du schon ein Benutzerkonto? Melde dich hier an", "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s oder %(usernamePassword)s", @@ -2945,13 +2945,13 @@ "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "Du kannst in den benutzerdefinierten Serveroptionen eine andere Heimserver-URL angeben, um dich bei anderen Matrixservern anzumelden.", "Server Options": "Servereinstellungen", "No other application is using the webcam": "keine andere Anwendung auf die Webcam zugreift", - "Permission is granted to use the webcam": "Zugriff auf die Webcam ist gestattet.", + "Permission is granted to use the webcam": "Zugriff auf Webcam gestattet", "A microphone and webcam are plugged in and set up correctly": "Mikrofon und Webcam eingesteckt und richtig eingerichtet sind", "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon zugegriffen werden konnte. Stelle sicher, dass das Mikrofon richtig eingesteckt und eingerichtet ist.", "Call failed because no webcam or microphone could not be accessed. Check that:": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon oder die Webcam zugegriffen werden konnte. Stelle sicher, dass:", "Unable to access webcam / microphone": "Auf Webcam / Mikrofon konnte nicht zugegriffen werden", "Unable to access microphone": "Es konnte nicht auf das Mikrofon zugegriffen werden", - "Host account on": "Benutzer*innenkonto betreiben an", + "Host account on": "Konto betreiben auf", "Hold": "Halten", "Resume": "Fortsetzen", "We call the places where you can host your account ‘homeservers’.": "Den Ort, an dem du dein Konto betreibst, nennen wir „Heimserver“.", @@ -2961,9 +2961,9 @@ "%(peerName)s held the call": "%(peerName)s hält den Anruf", "You held the call Resume": "Du hältst den Anruf Fortsetzen", "sends fireworks": "sendet Feuerwerk", - "Sends the given message with fireworks": "Sendet die gewählte Nachricht mit Feuerwerk", + "Sends the given message with fireworks": "Sendet die Nachricht mit Feuerwerk", "sends confetti": "sendet Konfetti", - "Sends the given message with confetti": "Sendet die gewählte Nachricht mit Konfetti", + "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti", "Show chat effects": "Chat-Effekte anzeigen", "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Stellt ┬──┬ ノ( ゜-゜ノ) einer Klartextnachricht voran", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Stellt (╯°□°)╯︵ ┻━┻ einer Klartextnachricht voran", @@ -2976,7 +2976,7 @@ "%(name)s on hold": "%(name)s wird gehalten", "You held the call Switch": "Du hältst den Anruf Wechseln", "sends snowfall": "sendet Schneeflocken", - "Sends the given message with snowfall": "Sendet die gewählte Nachricht mit Schneeflocken", + "Sends the given message with snowfall": "Sendet die Nachricht mit Schneeflocken", "Transfer": "Übertragen", "Failed to transfer call": "Anruf-Übertragung fehlgeschlagen", "A call can only be transferred to a single user.": "Ein Anruf kann nur auf einen einzelnen Nutzer übertragen werden.", @@ -3059,7 +3059,7 @@ "Cookie Policy": "Cookie-Richtlinie", "Learn more in our , and .": "Erfahre mehr in unserer , und .", "Failed to connect to your homeserver. Please close this dialog and try again.": "Verbindung zum Homeserver fehlgeschlagen. Bitte schließe diesen Dialog and versuche es erneut.", - "Abort": "Abbrechen", + "Abort": "Beenden", "Upgrade to %(hostSignupBrand)s": "Zu %(hostSignupBrand)s upgraden", "Edit Values": "Werte bearbeiten", "Value in this room:": "Wert in diesem Raum:", @@ -3084,7 +3084,7 @@ "Accept Invite": "Einladung akzeptieren", "Save changes": "Änderungen speichern", "Undo": "Rückgängig", - "Save Changes": "Änderungen Speichern", + "Save Changes": "Speichern", "View dev tools": "Entwicklerwerkzeuge", "Apply": "Anwenden", "Create a new room": "Neuen Raum erstellen", @@ -3239,9 +3239,9 @@ "Values at explicit levels in this room:": "Werte für explizite Stufen in diesem Raum:", "Values at explicit levels:": "Werte für explizite Stufen:", "Values at explicit levels in this room": "Werte für explizite Stufen in diesem Raum", - "Confirm abort of host creation": "Bestätige das Abbrechen der Host-Erstellung", - "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Soll die Host-Erstellung wirklich abgebrochen werden? Dieser Prozess kann nicht wieder fortgesetzt werden.", - "Invite to just this room": "Nur für diesen Raum einladen", + "Confirm abort of host creation": "Bestätige das Beenden der Host-Erstellung", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Soll die Host-Erstellung wirklich beendet werden? Dieser Prozess kann nicht wieder fortgesetzt werden.", + "Invite to just this room": "Nur in diesen Raum einladen", "Consult first": "Konsultiere zuerst", "Reset event store?": "Ereignisspeicher zurück setzen?", "You most likely do not want to reset your event index store": "Es ist wahrscheinlich, dass du den Ereignis-Indexspeicher nicht zurück setzen möchtest", @@ -3253,7 +3253,7 @@ "What are some things you want to discuss in %(spaceName)s?": "Welche Themen willst du in %(spaceName)s besprechen?", "Inviting...": "Einladen...", "Failed to create initial space rooms": "Fehler beim Initialisieren des Space", - "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Du bist hier noch alleine. Wenn du den Space verlässt, ist er für immer verloren (eine lange Zeit).", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Du bist die einzige Person hier. Wenn du ihn jetzt verlässt, ist er für immer verloren (eine lange Zeit).", "Edit settings relating to your space.": "Einstellungen vom Space bearbeiten.", "Please choose a strong password": "Bitte gib ein sicheres Passwort ein", "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Wenn du alles zurücksetzt, gehen alle verifizierten Anmeldungen, Benutzer und verschlüsselte Nachrichten verloren.", @@ -3282,5 +3282,66 @@ "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": "Falls du es wirklich willst: Es werden keine Nachrichten gelöscht. Außerdem wird die Suche, während der Index erstellt wird, etwas langsamer sein", "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s Mitglieder inklusive %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Inklusive%(commaSeparatedMembers)s", - "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Beratung mit %(transferTarget)s. Übertragung zu %(transferee)s" + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Beratung mit %(transferTarget)s. Übertragung zu %(transferee)s", + "Play": "Abspielen", + "Pause": "Pause", + "What do you want to organise?": "Was willst du organisieren?", + "Enter your Security Phrase a second time to confirm it.": "Gib dein Kennwort ein zweites Mal zur Bestätigung ein.", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Wähle Räume oder Konversationen die Du hinzufügen möchtest. Dieser Bereich ist nur für Dich, niemand wird informiert. Du kannst später mehr hinzufügen.", + "Filter all spaces": "Alle Spaces durchsuchen", + "Delete recording": "Aufnahme löschen", + "Stop the recording": "Aufnahme stoppen", + "%(count)s results in all spaces|one": "%(count)s Ergebnis", + "%(count)s results in all spaces|other": "%(count)s Ergebnisse", + "You have no ignored users.": "Du ignorierst keine Benutzer.", + "Error processing voice message": "Fehler beim Verarbeiten der Sprachnachricht", + "To join %(spaceName)s, turn on the Spaces beta": "Um %(spaceName)s beizutreten, aktiviere die Spaces Betaversion", + "To view %(spaceName)s, turn on the Spaces beta": "Um %(spaceName)s zu betreten, aktiviere die Spaces Beta", + "Select a room below first": "Wähle zuerst einen Raum aus", + "Communities are changing to Spaces": "Spaces ersetzen Communities", + "Join the beta": "Beta beitreten", + "Leave the beta": "Beta verlassen", + "Beta": "Beta", + "Tap for more info": "Klicke für mehr Infos", + "Spaces is a beta feature": "Spaces sind noch in der Entwicklung und möglicherweise instabil", + "Want to add a new room instead?": "Willst du einen neuen Raum hinzufügen?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Raum wird hinzugefügt...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Räume werden hinzugefügt... (%(progress)s von %(count)s)", + "You can add existing spaces to a space.": "Du kannst existierende Spaces zu einem Space hinzfügen.", + "Feeling experimental?": "Willst du die Entwicklung von Element hautnah miterleben?", + "You are not allowed to view this server's rooms list": "Du darfst diese Raumliste nicht sehen", + "We didn't find a microphone on your device. Please check your settings and try again.": "Es konnte kein Mikrofon gefunden werden. Überprüfe deine Einstellungen und versuche es erneut.", + "No microphone found": "Kein Mikrofon gefunden", + "We were unable to access your microphone. Please check your browser settings and try again.": "Fehler beim Zugriff auf dein Mikrofon. Überprüfe deine Browsereinstellungen und versuche es nochmal.", + "Unable to access your microphone": "Fehler beim Zugriff auf Mikrofon", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Hier kannst du zukünftige Features noch vor der Veröffentlichung testen und uns mit Feedback beim Verbessern helfen. Mehr Infos.", + "Please enter a name for the space": "Gib den Namen des Spaces ein", + "Connecting": "Verbinden", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Direktverbindung für Direktanrufe aktivieren. Dadurch sieht dein Gegenüber möglicherweise deine IP-Adresse.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Die Betaversion ist verfügbar für Browser, Desktop und Android. Je nach Homeserver sind einige Funktionen möglicherweise nicht verfügbar.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Du kannst die Betaversion jederzeit verlassen. Mache dies entweder in den Einstellungen oder klicke auf eines der \"Beta\"-Icons wie das hier oben.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s wird mit aktivierten Spaces neuladen. Danach kannst Communities und Custom Tags nicht verwenden.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Die Betaversion ist für Browser, Desktop und Android verfügbar. Danke, dass Du die Betaversion testest.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s wird mit deaktivierten Spaces neuladen und du kannst Communities und Custom Tags wieder verwenden können.", + "Spaces are a beta feature.": "Spaces sind in der Beta.", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Wir haben Spaces entwickelt, damit ihr eure Räume besser organisieren könnt. Um einen existierenden Space beitreten zu können musst du (noch) von jemandem eingeladen werden.", + "Spaces are a new way to group rooms and people.": "Wir haben Spaces entwickelt, damit ihr eure Räume besser organisieren könnt.", + "Message search initialisation failed": "Initialisierung der Nachrichtensuche fehlgeschlagen", + "Send and receive voice messages": "Sprachnachrichten", + "Search names and descriptions": "Nach Name und Beschreibung filtern", + "Not all selected were added": "Nicht alle Ausgewählten konnten hinzugefügt werden", + "Add reaction": "Reaktion hinzufügen", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Dieses Feature ist experimentell. Falls du eine Einladung erhältst musst du sie momentan noch auf öffnen, um beizutreten.", + "You may contact me if you have any follow up questions": "Kontaktiert mich, falls ihr weitere Fragen zu meinem Feedback habt", + "To leave the beta, visit your settings.": "Du kannst die Beta in den Einstellungen deaktivieren.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Die Platform von Element und dein Benutzername werden mitgeschickt, damit wir dein Feedback bestmöglich nachvollziehen können.", + "%(featureName)s beta feedback": "%(featureName)s-Beta Feedback", + "Thank you for your feedback, we really appreciate it.": "Uns liegt es am Herzen, Element zu verbessern. Deshalb ein großes Danke für dein Feedback.", + "Beta feedback": "Beta Feedback", + "Your access token gives full access to your account. Do not share it with anyone.": "Dein Zugriffstoken gibt vollen Zugriff auf dein Konto. Teile es niemals mit jemanden anderen.", + "Access Token": "Zugriffstoken", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Dein Feedback hilfst uns, die Spaces zu verbessern. Je genauer, desto besser.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Durchs Verlassen lädt %(brand)s mit deaktivierten Spaces neu. Danach kannst du Communities und Custom Tags wieder verwenden.", + "sends space invaders": "sendet Space Invaders", + "Sends the given message with a space themed effect": "Sendet die Nachricht mit Raumschiffen" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dcad970300..9e85ea28c8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -37,6 +37,8 @@ "Call Failed": "Call Failed", "Call Declined": "Call Declined", "The other party declined the call.": "The other party declined the call.", + "User Busy": "User Busy", + "The user you called is busy.": "The user you called is busy.", "The remote side failed to pick up": "The remote side failed to pick up", "The call could not be established": "The call could not be established", "Answered Elsewhere": "Answered Elsewhere", @@ -61,6 +63,8 @@ "Already in call": "Already in call", "You're already in a call with this person.": "You're already in a call with this person.", "You cannot place a call with yourself.": "You cannot place a call with yourself.", + "Unable to look up phone number": "Unable to look up phone number", + "There was an error looking up the phone number": "There was an error looking up the phone number", "Call in Progress": "Call in Progress", "A call is currently being placed!": "A call is currently being placed!", "Permission Required": "Permission Required", @@ -578,14 +582,6 @@ "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", "Light": "Light", "Dark": "Dark", - "You signed in to a new session without verifying it:": "You signed in to a new session without verifying it:", - "Verify your other session using one of the options below.": "Verify your other session using one of the options below.", - "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:", - "Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.", - "Not Trusted": "Not Trusted", - "Manually Verify by Text": "Manually Verify by Text", - "Interactively verify by Emoji": "Interactively verify by Emoji", - "Done": "Done", "%(displayName)s is typing …": "%(displayName)s is typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", @@ -608,6 +604,10 @@ "See when the avatar changes in this room": "See when the avatar changes in this room", "Change the avatar of your active room": "Change the avatar of your active room", "See when the avatar changes in your active room": "See when the avatar changes in your active room", + "Kick, ban, or invite people to this room, and make you leave": "Kick, ban, or invite people to this room, and make you leave", + "See when people join, leave, or are invited to this room": "See when people join, leave, or are invited to this room", + "Kick, ban, or invite people to your active room, and make you leave": "Kick, ban, or invite people to your active room, and make you leave", + "See when people join, leave, or are invited to your active room": "See when people join, leave, or are invited to your active room", "Send stickers to this room as you": "Send stickers to this room as you", "See when a sticker is posted in this room": "See when a sticker is posted in this room", "Send stickers to your active room as you": "Send stickers to your active room as you", @@ -785,11 +785,18 @@ "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "Change notification settings": "Change notification settings", "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.", + "Spaces": "Spaces", + "Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta available for web, desktop and Android. Thank you for trying the beta.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", - "Send and receive voice messages (in development)": "Send and receive voice messages (in development)", + "Send and receive voice messages": "Send and receive voice messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages", "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.", - "New spinner design": "New spinner design", "Message Pinning": "Message Pinning", "Custom user status messages": "Custom user status messages", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", @@ -880,6 +887,8 @@ "sends fireworks": "sends fireworks", "Sends the given message with snowfall": "Sends the given message with snowfall", "sends snowfall": "sends snowfall", + "Sends the given message with a space themed effect": "Sends the given message with a space themed effect", + "sends space invaders": "sends space invaders", "unknown person": "unknown person", "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", "You held the call Switch": "You held the call Switch", @@ -891,8 +900,6 @@ "Fill Screen": "Fill Screen", "Return to call": "Return to call", "%(name)s on hold": "%(name)s on hold", - "Unable to look up phone number": "Unable to look up phone number", - "There was an error looking up the phone number": "There was an error looking up the phone number", "Dial pad": "Dial pad", "Unknown caller": "Unknown caller", "Incoming voice call": "Incoming voice call", @@ -996,8 +1003,9 @@ "Upload": "Upload", "Name": "Name", "Description": "Description", + "Please enter a name for the space": "Please enter a name for the space", "Create a space": "Create a space", - "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.", "Public": "Public", "Open space for anyone, best for communities": "Open space for anyone, best for communities", "Private": "Private", @@ -1012,7 +1020,7 @@ "Create": "Create", "Expand space panel": "Expand space panel", "Collapse space panel": "Collapse space panel", - "Home": "Home", + "All rooms": "All rooms", "Click to copy": "Click to copy", "Copied!": "Copied!", "Failed to copy": "Failed to copy", @@ -1088,7 +1096,7 @@ "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.", - "Message search initilisation failed": "Message search initilisation failed", + "Message search initialisation failed": "Message search initialisation failed", "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", @@ -1258,7 +1266,7 @@ "Copy": "Copy", "Clear cache and reload": "Clear cache and reload", "Labs": "Labs", - "Customise your experience with experimental labs features. Learn more.": "Customise your experience with experimental labs features. Learn more.", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.", "Ignored/Blocked": "Ignored/Blocked", "Error adding ignored user/server": "Error adding ignored user/server", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", @@ -1441,6 +1449,13 @@ "Someone is using an unknown session": "Someone is using an unknown session", "This room is end-to-end encrypted": "This room is end-to-end encrypted", "Everyone in this room is verified": "Everyone in this room is verified", + "Server error": "Server error", + "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", + "Unknown Command": "Unknown Command", + "Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s", + "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", + "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", + "Send as message": "Send as message", "Edit message": "Edit message", "Mod": "Mod", "This event could not be displayed": "This event could not be displayed", @@ -1495,11 +1510,8 @@ "Invite to just this room": "Invite to just this room", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", - "No pinned messages.": "No pinned messages.", - "Loading...": "Loading...", - "Pinned Messages": "Pinned Messages", - "Unpin Message": "Unpin Message", - "Jump to message": "Jump to message", + "Unpin": "Unpin", + "View message": "View message", "%(duration)ss": "%(duration)ss", "%(duration)sm": "%(duration)sm", "%(duration)sh": "%(duration)sh", @@ -1631,13 +1643,6 @@ "This Room": "This Room", "All Rooms": "All Rooms", "Search…": "Search…", - "Server error": "Server error", - "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", - "Unknown Command": "Unknown Command", - "Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s", - "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", - "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", - "Send as message": "Send as message", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", @@ -1712,9 +1717,11 @@ "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to", "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection", "Yours, or the other users’ session": "Yours, or the other users’ session", + "You’re all caught up": "You’re all caught up", + "You have no visible notifications.": "You have no visible notifications.", + "Pinned messages": "Pinned messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", - "Unpin": "Unpin", "Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel", "Options": "Options", "Set my room layout for everyone": "Set my room layout for everyone", @@ -1855,6 +1862,7 @@ "You sent a verification request": "You sent a verification request", "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", + "Add reaction": "Add reaction", "Show all": "Show all", "Reactions": "Reactions", " reacted with %(content)s": " reacted with %(content)s", @@ -1889,6 +1897,7 @@ "Add rooms to this community": "Add rooms to this community", "Filter community rooms": "Filter community rooms", "Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.", + "Loading...": "Loading...", "Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.", "You're not currently a member of any communities.": "You're not currently a member of any communities.", "Frequently Used": "Frequently Used", @@ -1939,11 +1948,10 @@ "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "Zoom out": "Zoom out", "Zoom in": "Zoom in", - "Rotate Right": "Rotate Right", "Rotate Left": "Rotate Left", + "Rotate Right": "Rotate Right", "Download": "Download", "Information": "Information", - "View message": "View message", "Language Dropdown": "Language Dropdown", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", @@ -2016,10 +2024,11 @@ "Continue with %(provider)s": "Continue with %(provider)s", "Sign in with single sign-on": "Sign in with single sign-on", "And %(count)s more...|other": "And %(count)s more...", + "Home": "Home", "Enter a server name": "Enter a server name", "Looks good": "Looks good", + "You are not allowed to view this server's rooms list": "You are not allowed to view this server's rooms list", "Can't find this server or its room list": "Can't find this server or its room list", - "All rooms": "All rooms", "Your server": "Your server", "Are you sure you want to remove %(serverName)s": "Are you sure you want to remove %(serverName)s", "Remove server": "Remove server", @@ -2030,14 +2039,15 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", - "Filter your rooms and spaces": "Filter your rooms and spaces", - "Spaces": "Spaces", - "Direct Messages": "Direct Messages", - "Space selection": "Space selection", - "Add existing rooms": "Add existing rooms", "Not all selected were added": "Not all selected were added", "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", + "Filter your rooms and spaces": "Filter your rooms and spaces", + "Feeling experimental?": "Feeling experimental?", + "You can add existing spaces to a space.": "You can add existing spaces to a space.", + "Direct Messages": "Direct Messages", + "Space selection": "Space selection", + "Add existing rooms": "Add existing rooms", "Want to add a new room instead?": "Want to add a new room instead?", "Create a new room": "Create a new room", "Matrix ID": "Matrix ID", @@ -2053,6 +2063,15 @@ "Invite anyway and never warn me again": "Invite anyway and never warn me again", "Invite anyway": "Invite anyway", "Close dialog": "Close dialog", + "Beta feedback": "Beta feedback", + "Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.", + "Done": "Done", + "%(featureName)s beta feedback": "%(featureName)s beta feedback", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.", + "To leave the beta, visit your settings.": "To leave the beta, visit your settings.", + "Feedback": "Feedback", + "You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions", + "Send feedback": "Send feedback", "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.", "Preparing to send logs": "Preparing to send logs", "Logs sent": "Logs sent", @@ -2183,10 +2202,8 @@ "Comment": "Comment", "There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.", "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", - "Feedback": "Feedback", "Report a bug": "Report a bug", "Please view existing bugs on Github first. No match? Start a new one.": "Please view existing bugs on Github first. No match? Start a new one.", - "Send feedback": "Send feedback", "Confirm abort of host creation": "Confirm abort of host creation", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", "Abort": "Abort", @@ -2372,6 +2389,13 @@ "Summary": "Summary", "Document": "Document", "Next": "Next", + "You signed in to a new session without verifying it:": "You signed in to a new session without verifying it:", + "Verify your other session using one of the options below.": "Verify your other session using one of the options below.", + "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:", + "Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.", + "Not Trusted": "Not Trusted", + "Manually Verify by Text": "Manually Verify by Text", + "Interactively verify by Emoji": "Interactively verify by Emoji", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files": "Upload files", "Upload all": "Upload all", @@ -2441,6 +2465,7 @@ "Unable to reject invite": "Unable to reject invite", "Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)", "Forward Message": "Forward Message", + "Unpin Message": "Unpin Message", "Pin Message": "Pin Message", "Unhide Preview": "Unhide Preview", "Share Permalink": "Share Permalink", @@ -2464,6 +2489,11 @@ "Revoke permissions": "Revoke permissions", "Move left": "Move left", "Move right": "Move right", + "Spaces is a beta feature": "Spaces is a beta feature", + "Tap for more info": "Tap for more info", + "Beta": "Beta", + "Leave the beta": "Leave the beta", + "Join the beta": "Join the beta", "Avatar": "Avatar", "This room is public": "This room is public", "Away": "Away", @@ -2597,8 +2627,7 @@ "Error whilst fetching joined communities": "Error whilst fetching joined communities", "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", - "You’re all caught up": "You’re all caught up", - "You have no visible notifications.": "You have no visible notifications.", + "Communities are changing to Spaces": "Communities are changing to Spaces", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", "%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.", "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.", @@ -2616,6 +2645,8 @@ "Unable to look up room ID from server": "Unable to look up room ID from server", "Preview": "Preview", "View": "View", + "No results for \"%(query)s\"": "No results for \"%(query)s\"", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.", "Find a room…": "Find a room…", "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "If you can't find the room you're looking for, ask for an invite or Create a new room.", @@ -2664,12 +2695,15 @@ "Mark as suggested": "Mark as suggested", "No results found": "No results found", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", - "Search names and description": "Search names and description", + "Search names and descriptions": "Search names and descriptions", "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", "Create room": "Create room", + "Spaces are a beta feature.": "Spaces are a beta feature.", "Public space": "Public space", "Private space": "Private space", " invites you": " invites you", + "To view %(spaceName)s, turn on the Spaces beta": "To view %(spaceName)s, turn on the Spaces beta", + "To join %(spaceName)s, turn on the Spaces beta": "To join %(spaceName)s, turn on the Spaces beta", "Welcome to ": "Welcome to ", "Random": "Random", "Support": "Support", @@ -2677,13 +2711,12 @@ "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", "Creating rooms...": "Creating rooms...", - "Failed to add rooms to space": "Failed to add rooms to space", - "Adding...": "Adding...", "What do you want to organise?": "What do you want to organise?", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", "Share %(name)s": "Share %(name)s", "It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.", "Go to my first room": "Go to my first room", + "Go to my space": "Go to my space", "Who are you working with?": "Who are you working with?", "Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s", "Just me": "Just me", @@ -2694,6 +2727,7 @@ "Inviting...": "Inviting...", "Invite your teammates": "Invite your teammates", "Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.", "Invite by username": "Invite by username", "What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?", "Let's create a room for each of them.": "Let's create a room for each of them.", @@ -2719,6 +2753,8 @@ "Switch theme": "Switch theme", "User menu": "User menu", "Community and user menu": "Community and user menu", + "Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms", + "Currently joining %(count)s rooms|one": "Currently joining %(count)s room", "Could not load user profile": "Could not load user profile", "Decrypted event source": "Decrypted event source", "Original event source": "Original event source", @@ -2806,6 +2842,7 @@ "Room Notification": "Room Notification", "Notification Autocomplete": "Notification Autocomplete", "Room Autocomplete": "Room Autocomplete", + "Space Autocomplete": "Space Autocomplete", "Users": "Users", "User Autocomplete": "User Autocomplete", "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 4a97b60a2e..a5d7756de8 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -665,5 +665,6 @@ "Unrecognised command: %(commandText)s": "Unrecognized command: %(commandText)s", "Add some details to help people recognise it.": "Add some details to help people recognize it.", "Unrecognised room address:": "Unrecognized room address:", - "A private space to organise your rooms": "A private space to organize your rooms" + "A private space to organise your rooms": "A private space to organize your rooms", + "Message search initialisation failed": "Message search initialization failed" } diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index f4d30b40b7..fc8c00fd19 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -324,7 +324,7 @@ "Add rooms to this community": "Aldoni ĉambrojn al ĉi tiu komunumo", "An email has been sent to %(emailAddress)s": "Retletero sendiĝis al %(emailAddress)s", "Please check your email to continue registration.": "Bonvolu kontroli vian retpoŝton por daŭrigi la registriĝon.", - "Token incorrect": "Malĝusta ĵetono", + "Token incorrect": "Malĝusta peco", "A text message has been sent to %(msisdn)s": "Tekstmesaĝo sendiĝîs al %(msisdn)s", "Please enter the code it contains:": "Bonvolu enigi la enhavatan kodon:", "Start authentication": "Komenci aŭtentikigon", @@ -769,7 +769,7 @@ "Failed to invite users to the room:": "Malsukcesis inviti uzantojn al la ĉambro:", "Opens the Developer Tools dialog": "Maflermas evoluigistan interagujon", "This homeserver has hit its Monthly Active User limit.": "Tiu ĉi hejmservilo atingis sian monatan limon de aktivaj uzantoj.", - "This homeserver has exceeded one of its resource limits.": "Tiu ĉi hejmservilo superis je unu el siaj risurcaj limoj.", + "This homeserver has exceeded one of its resource limits.": "Tiu ĉi hejmservilo superis je unu el siaj rimedaj limoj.", "Unable to connect to Homeserver. Retrying...": "Ne povas konektiĝi al hejmservilo. Reprovante…", "You do not have permission to invite people to this room.": "Vi ne havas permeson inviti personojn al la ĉambro.", "User %(user_id)s does not exist": "Uzanto %(user_id)s ne ekzistas", @@ -1569,7 +1569,7 @@ "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Bloki aliĝojn al ĉi tiu ĉambro de uzantoj el aliaj Matrix-serviloj (Ĉi tiun agordon ne eblas poste ŝanĝi!)", "Please fill why you're reporting.": "Bonvolu skribi, kial vi raportas.", "Report Content to Your Homeserver Administrator": "Raporti enhavon al la administrantode via hejmservilo", - "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Per raporto de ĉi tiu mesaĝo vi sendos ĝian unikan « eventan identigilon » al la administranto de via hejmservilo. Se mesaĝoj en ĉi tiu ĉambro estas ĉifrataj, la administranto de via hejmservilo ne povos legi la tekston de la mesaĝo, nek rigardi dosierojn aŭ bildojn.", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Per raporto de ĉi tiu mesaĝo vi sendos ĝian unikan «identigilon de okazo» al la administranto de via hejmservilo. Se mesaĝoj en ĉi tiu ĉambro estas ĉifrataj, la administranto de via hejmservilo ne povos legi la tekston de la mesaĝo, nek rigardi dosierojn aŭ bildojn.", "Send report": "Sendi raporton", "Command Help": "Helpo pri komando", "To continue you need to accept the terms of this service.": "Por pluigi, vi devas akcepti la uzokondiĉojn de ĉi tiu servo.", @@ -1653,7 +1653,7 @@ "Unencrypted": "Neĉifrita", "Send a reply…": "Sendi respondon…", "Send a message…": "Sendi mesaĝon…", - "Direct Messages": "Rektaj ĉambroj", + "Direct Messages": "Individuaj ĉambroj", " wants to chat": " volas babili", "Start chatting": "Ekbabili", "Reject & Ignore user": "Rifuzi kaj malatenti uzanton", @@ -1665,7 +1665,7 @@ "Start Verification": "Komenci kontrolon", "Trusted": "Fidata", "Not trusted": "Nefidata", - "Direct message": "Rekta ĉambro", + "Direct message": "Individua ĉambro", "Security": "Sekureco", "Reactions": "Reagoj", "More options": "Pliaj elektebloj", @@ -1919,7 +1919,7 @@ "Failed to find the following users": "Malsukcesis trovi la jenajn uzantojn", "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "La jenaj uzantoj eble ne ekzistas aŭ ne validas, kaj ne povas invitiĝi: %(csvNames)s", "Recent Conversations": "Freŝaj interparoloj", - "Recently Direct Messaged": "Freŝaj rektaj ĉambroj", + "Recently Direct Messaged": "Freŝe uzitaj individuaj ĉambroj", "Go": "Iri", "Your account is not secure": "Via konto ne estas sekura", "Your password": "Via pasvorto", @@ -2312,7 +2312,7 @@ "Customise your appearance": "Adaptu vian aspekton", "Appearance Settings only affect this %(brand)s session.": "Agordoj de aspekto nur efikos sur ĉi tiun salutaĵon de %(brand)s.", "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.": "Aldonu uzantojn kaj servilojn, kiujn vi volas malatenti, ĉi tien. Uzu steletojn por ke %(brand)s atendu iujn ajn signojn. Ekzemple, @bot:* malatentigus ĉiujn uzantojn, kiuj havas la nomon «bot» sur ĉiu ajn servilo.", - "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "La administranto de via servilo malŝaltis implicitan tutvojan ĉifradon en privataj kaj rektaj ĉambroj.", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "La administranto de via servilo malŝaltis implicitan tutvojan ĉifradon en privataj kaj individuaj ĉambroj.", "Make this room low priority": "Doni al la ĉambro malaltan prioritaton", "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Ĉambroj kun malalta prioritato montriĝas en aparta sekcio, en la suba parto de via ĉambrobreto,", "The authenticity of this encrypted message can't be guaranteed on this device.": "La aŭtentikeco de ĉi tiu ĉifrita mesaĝo ne povas esti garantiita sur ĉi tiu aparato.", @@ -2391,7 +2391,7 @@ "The person who invited you already left the room.": "La persono, kiu vin invitis, jam foriris de la ĉambro.", "The person who invited you already left the room, or their server is offline.": "Aŭ la persono, kiu vin invitis, jam foriris de la ĉambro, aŭ ĝia servilo estas eksterreta.", "Change notification settings": "Ŝanĝi agordojn pri sciigoj", - "Show message previews for reactions in DMs": "Montri antaŭrigardojn al mesaĝoj ĉe reagoj en rektaj ĉambroj", + "Show message previews for reactions in DMs": "Montri antaŭrigardojn al mesaĝoj ĉe reagoj en individuaj ĉambroj", "Show message previews for reactions in all rooms": "Montri antaŭrigardojn al mesaĝoj ĉe reagoj en ĉiuj ĉambroj", "Your server isn't responding to some requests.": "Via servilo ne respondas al iuj petoj.", "Server isn't responding": "Servilo ne respondas", @@ -2729,10 +2729,10 @@ "Send images as you in your active room": "Sendi bildojn kiel vi en via aktiva ĉambro", "Send images as you in this room": "Sendi bildojn kiel vi en ĉi tiu ĉambro", "The %(capability)s capability": "La kapablo %(capability)s", - "See %(eventType)s events posted to your active room": "Vidi eventojn de speco %(eventType)s afiŝitajn al via aktiva ĉambro", - "Send %(eventType)s events as you in your active room": "Sendi eventojn de speco %(eventType)s kiel vi en via aktiva ĉambro", - "See %(eventType)s events posted to this room": "Vidi eventojn de speco %(eventType)s afiŝitajn al ĉi tiu ĉambro", - "Send %(eventType)s events as you in this room": "Sendi eventojn de speco %(eventType)s kiel vi en ĉi tiu ĉambro", + "See %(eventType)s events posted to your active room": "Vidi okazojn de speco %(eventType)s afiŝitajn al via aktiva ĉambro", + "Send %(eventType)s events as you in your active room": "Sendi okazojn de speco %(eventType)s kiel vi en via aktiva ĉambro", + "See %(eventType)s events posted to this room": "Vidi okazojn de speco %(eventType)s afiŝitajn al ĉi tiu ĉambro", + "Send %(eventType)s events as you in this room": "Sendi okazojn de speco %(eventType)s kiel vi en ĉi tiu ĉambro", "See messages posted to your active room": "Vidi mesaĝojn senditajn al via aktiva ĉambro", "See messages posted to this room": "Vidi mesaĝojn senditajn al ĉi tiu ĉambro", "Send messages as you in your active room": "Sendi mesaĝojn kiel vi en via aktiva ĉambro", @@ -3000,14 +3000,14 @@ "Send text messages as you in this room": "Sendi tekstajn mesaĝojn kiel vi en ĉi tiu ĉambro", "Change which room, message, or user you're viewing": "Ŝanĝu, kiun ĉambron, mesaĝon, aŭ uzanton vi rigardas", "%(senderName)s has updated the widget layout": "%(senderName)s ĝisdatigis la aranĝon de la fenestrajoj", - "Converts the DM to a room": "Igas la ĉambron nerekta", - "Converts the room to a DM": "Igas la ĉambron rekta", + "Converts the DM to a room": "Malindividuigas la ĉambron", + "Converts the room to a DM": "Individuigas la ĉambron", "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Via hejmservilo rifuzis vian saluton. Eble tio okazis, ĉar ĝi simple daŭris tro longe. Bonvolu reprovi. Se tio daŭros, bonvolu kontakti la administranton de via hejmservilo.", "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Via hejmservilo estis neatingebla kaj ne povis vin salutigi. Bonvolu reprovi. Se tio daŭros, bonvolu kontakti la administranton de via hejmservilo.", "Try again": "Reprovu", "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Ni petis la foliumilon memori, kiun hejmservilon vi uzas por saluti, sed domaĝe, via foliumilo forgesis. Iru al la saluta paĝo kaj reprovu.", "We couldn't log you in": "Ni ne povis salutigi vin", - "%(creator)s created this DM.": "%(creator)s kreis ĉi tiun rektan ĉambron.", + "%(creator)s created this DM.": "%(creator)s kreis ĉi tiun individuan ĉambron.", "Invalid URL": "Nevalida URL", "Unable to validate homeserver": "Ne povas validigi hejmservilon", "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Averte, se vi ne aldonos retpoŝtadreson kaj poste forgesos vian pasvorton, vi eble por ĉiam perdos aliron al via konto.", @@ -3156,8 +3156,8 @@ "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Pratipo de Aroj. Malkonforma kun Komunumoj, Komunumoj v2, kaj Propraj etikedoj. Bezonas konforman hejmservilon por iuj funkcioj.", "Verify this login to access your encrypted messages and prove to others that this login is really you.": "Kontrolu ĉi tiun saluton por aliri viajn ĉifritajn mesaĝojn, kaj pruvi al aliuloj, ke la salutanto vere estas vi.", "Verify with another session": "Knotroli per alia salutaĵo", - "Original event source": "Originala fonto de evento", - "Decrypted event source": "Malĉifrita fonto de evento", + "Original event source": "Originala fonto de okazo", + "Decrypted event source": "Malĉifrita fonto de okazo", "We'll create rooms for each of them. You can add more later too, including already existing ones.": "Por ĉiu el ili ni kreos ĉambron. Vi povos aldoni pliajn pli poste, inkluzive jam ekzistantajn.", "What projects are you working on?": "Kiujn projektojn vi prilaboras?", "Let's create a room for each of them. You can add more later too, including already existing ones.": "Ni kreu ĉambron por ĉiu el ili. Vi povas aldoni pliajn poste, inkluzive jam ekzistantajn.", @@ -3218,5 +3218,107 @@ "Show options to enable 'Do not disturb' mode": "Montri elekteblojn por ŝalti sendistran reĝimon", "%(deviceId)s from %(ip)s": "%(deviceId)s de %(ip)s", "Review to ensure your account is safe": "Kontrolu por certigi sekurecon de via konto", - "Sends the given message as a spoiler": "Sendas la donitan mesaĝon kiel malkaŝon de intrigo" + "Sends the given message as a spoiler": "Sendas la donitan mesaĝon kiel malkaŝon de intrigo", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Ĉu vi certe volas nuligi kreadon de la gastiganto? Ĉi tiu procedo ne estos daŭrigebla.", + "Confirm abort of host creation": "Konfirmu nuligon de kreado de gastiganto", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Ĉu vi eksperimentemas? Laboratorioj estas la plej bona maniero frue akiri kaj testi novajn funkciojn, kaj helpi ilin formi antaŭ ilia plena ekuzo. Eksciu plion.", + "Your access token gives full access to your account. Do not share it with anyone.": "Via alirpeco donas plenan aliron al via konto. Donu ĝin al neniu.", + "We couldn't create your DM.": "Ni ne povis krei vian individuan ĉambron.", + "You may contact me if you have any follow up questions": "Vi povas min kontakti okaze de pliaj demandoj", + "To leave the beta, visit your settings.": "Por foriri de la prova versio, iru al viaj agordoj.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Via platformo kaj uzantonomo helpos al ni pli bone uzi viajn prikomentojn.", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Viaj prikomentoj helpos plibonigi arojn. Kiom pli detale vi skribos, tiom pli bonos.", + "%(featureName)s beta feedback": "Komentoj pri la prova versio de %(featureName)s", + "Thank you for your feedback, we really appreciate it.": "Dankon pro viaj prikomentoj, ni vere ilin ŝatas.", + "Beta feedback": "Komentoj pri la prova versio", + "Want to add a new room instead?": "Ĉu vi volas anstataŭe aldoni novan ĉambron?", + "You can add existing spaces to a space.": "Vi povas arigi arojn.", + "Feeling experimental?": "Ĉu vi eksperimentemas?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Aldonante ĉambron…", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Aldonante ĉambrojn… (%(progress)s el %(count)s)", + "Not all selected were added": "Ne ĉiuj elektitoj aldoniĝis", + "You are not allowed to view this server's rooms list": "Vi ne rajtas vidi liston de ĉambroj de tu ĉi servilo", + "Add reaction": "Aldoni reagon", + "Error processing voice message": "Eraris traktado de voĉmesaĝo", + "Delete recording": "Forigi registraĵon", + "Stop the recording": "Ĉesigi la registradon", + "We didn't find a microphone on your device. Please check your settings and try again.": "Ni ne trovis mikrofonon en via aparato. Bonvolu kontroli viajn agordojn kaj reprovi.", + "No microphone found": "Neniu mikrofono troviĝis", + "We were unable to access your microphone. Please check your browser settings and try again.": "Ni ne povis aliri vian mikrofonon. Bonvolu kontroli la agordojn de via foliumilo kaj reprovi.", + "Unable to access your microphone": "Ne povas aliri vian mikrofonon", + "%(count)s results in all spaces|one": "%(count)s rezulto en ĉiuj aroj", + "%(count)s results in all spaces|other": "%(count)s rezultoj en ĉiuj aroj", + "You have no ignored users.": "Vi malatentas neniujn uzantojn.", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Aroj prezentas novan manieron grupigi ĉambrojn kaj personojn. Por aliĝi al jama spaco, vi bezonos inviton.", + "Please enter a name for the space": "Bonvolu enigi nomon por la aro", + "Play": "Ludi", + "Pause": "Paŭzigi", + "Connecting": "Konektante", + "Sends the given message with a space themed effect": "Sendas mesaĝon kun la efekto de kosmo", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Permesi samtavolajn individuajn vokojn (kaj do videbligi vian IP-adreson al la alia vokanto)", + "Send and receive voice messages": "Sendi kaj ricevi voĉmesaĝojn", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Prova versio disponeblas por reto, labortablo, kaj Androido. Iuj funkcioj eble ne disponeblas per via hejmservilo.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Vi povas forlasi la provan version iam ajn per la agordoj, aŭ per tuŝeto al la prova insigno, kiel tiu ĉi-supre.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s estos enlegita kun subetno de Aroj. Komunumoj kaj propraj etikedoj iĝos kaŝitaj.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se vi foriros, %(brand)s estos enlegita sen subteno de Aroj. Komunumoj kaj propraj etikedoj ree estos videblaj.", + "Spaces are a new way to group rooms and people.": "Aroj prezentas novan manieron grupigi ĉambrojn kaj homojn.", + "See when people join, leave, or are invited to your active room": "Vidu kiam oni aliĝas, foriras, aŭ invitiĝas al via aktiva ĉambro", + "See when people join, leave, or are invited to this room": "Vidu kiam oni aliĝas, foriras, aŭ invitiĝas al la ĉambro", + "This homeserver has been blocked by it's administrator.": "Tiu ĉi hejmservilo estas blokita de sia administranto.", + "This homeserver has been blocked by its administrator.": "Tiu ĉi hejmservilo estas blokita de sia administranto.", + "Modal Widget": "Reĝima fenestraĵo", + "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "Via mesaĝo ne sendiĝis, ĉar ĉi tiu hejmservilo estas blokita de ĝia administranto. Bonvolu kontakti la administranton de via servo por daŭre uzadi la servon.", + "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Elemento por la reto estas eksperimenta sur telefono. Por pli bona sperto kaj freŝaj funkcioj, uzu nian senpagan malfremdan aplikaĵon.", + "Kick, ban, or invite people to your active room, and make you leave": "Forpeli, forbari, aŭ inviti homojn al via aktiva ĉambro, kaj foririgi vin", + "Kick, ban, or invite people to this room, and make you leave": "Forpeli, forbari, aŭ inviti personojn al la ĉambro, kaj foririgi vin", + "Consult first": "Unue konsulti", + "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Provizora daŭrigo permesas al la agorda procedo de %(hostSignupBrand)s aliri vian konton por preni kontrolitajn retpoŝtadresojn. Tiuj ĉi datumoj de konserviĝos.", + "Access Token": "Alirpeco", + "Message search initialisation failed": "Malsukcesis komenci serĉadon de mesaĝoj", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Konsultante kun %(transferTarget)s. Transdono al %(transferee)s", + "sends space invaders": "sendas imiton de ludo « Space Invaders »", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Prova versio disponeblas por reto, labortablo, kaj Androido. Dankon pro via provo.", + "Enter your Security Phrase a second time to confirm it.": "Enigu vian Sekurecan frazon duafoje por ĝin konfirmi.", + "Space Autocomplete": "Memaga finfaro de aro", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Sen kontrolo, vi ne povos aliri al ĉiuj viaj mesaĝoj, kaj aliuloj vin povos vidi nefidata.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Kontrolu vian identecon por aliri ĉifritajn mesaĝojn kaj pruvi vian identecon al aliuloj.", + "Use another login": "Uzi alian saluton", + "Please choose a strong password": "Bonvolu elekti fortan pasvorton", + "You can add more later too, including already existing ones.": "Vi povas aldoni pliajn poste, inkluzive tiujn, kiuj jam ekzistas.", + "Let's create a room for each of them.": "Kreu ni ĉambron por ĉiu el ili.", + "What are some things you want to discuss in %(spaceName)s?": "Pri kio volus vi diskuti en %(spaceName)s?", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Ĉi tio estas prova funkcio. Uzantoj, kiuj nun ricevos inviton, devos ĝin malfermi per por efektive aliĝi.", + "Go to my space": "Iri al mia aro", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Elektu aldonotajn ĉambrojn aŭ interparolojn. Ĉi tiu aro estas nur por vi, neniu estos informita. Vi povas aldoni pliajn pli poste.", + "What do you want to organise?": "Kion vi volas organizi?", + "Skip for now": "Preterpasi ĉi-foje", + "To join %(spaceName)s, turn on the Spaces beta": "Por aliĝi al %(spaceName)s, ŝaltu la provan version de Aroj", + "To view %(spaceName)s, turn on the Spaces beta": "Por vidi %(spaceName)s, ŝaltu la provan version de Aroj", + "Spaces are a beta feature.": "Aroj estas prova funkcio.", + "Search names and descriptions": "Serĉi nomojn kaj priskribojn", + "Select a room below first": "Unue elektu ĉambron de sube", + "You can select all or individual messages to retry or delete": "Vi povas elekti ĉiujn aŭ unuopajn mesaĝojn, por reprovi aŭ forigi", + "Sending": "Sendante", + "Retry all": "Reprovi ĉiujn", + "Delete all": "Forigi ĉiujn", + "Some of your messages have not been sent": "Kelkaj viaj mesaĝoj ne sendiĝis", + "Filter all spaces": "Filtri ĉiujn arojn", + "Communities are changing to Spaces": "Komunumoj iĝas Aroj", + "Verification requested": "Kontrolpeto", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Vi estas la nura persono tie ĉi. Se vi foriros, neniu alia plu povos aliĝi, inkluzive vin mem.", + "Avatar": "Profilbildo", + "Join the beta": "Aliĝi al provado", + "Leave the beta": "Ĉesi provadon", + "Beta": "Prova", + "Tap for more info": "Klaku por pliaj informoj", + "Spaces is a beta feature": "Aroj estas prova funkcio", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Se vi restarigos ĉion, vi rekomencos sen fidataj salutaĵoj, uzantoj, kaj eble ne povos vidi antaŭajn mesaĝojn.", + "Only do this if you have no other device to complete verification with.": "Faru tion ĉi nur se vi ne havas alian aparaton, per kiu vi kontrolus ceterajn.", + "Forgotten or lost all recovery methods? Reset all": "Ĉu vi forgesis aŭ perdis ĉiujn manierojn de rehavo? Restarigu ĉion", + "Reset everything": "Restarigi ĉion", + "Verify other login": "Kontroli alian saluton", + "Reset event store": "Restarigi deponejon de okazoj", + "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": "Se vi tamen tion faras, sciu ke neniu el viaj mesaĝoj foriĝos, sed via sperto pri serĉado povas malboniĝi momente, dum la indekso estas refarata", + "You most likely do not want to reset your event index store": "Plej probable, vi ne volas restarigi vian deponejon de indeksoj de okazoj", + "Reset event store?": "Ĉu restarigi deponejon de okazoj?" } diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 155aed321b..60f5d06bec 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -192,7 +192,7 @@ "Add a topic": "Añadir un tema", "No media permissions": "Sin permisos para el medio", "You may need to manually permit %(brand)s to access your microphone/webcam": "Probablemente necesites dar permisos manualmente a %(brand)s para tu micrófono/cámara", - "Are you sure you want to leave the room '%(roomName)s'?": "¿Salir de la sala «%(roomName)s?", + "Are you sure you want to leave the room '%(roomName)s'?": "¿Salir de la sala «%(roomName)s»?", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "No se puede conectar al servidor base. Por favor, comprueba tu conexión, asegúrate de que el certificado SSL del servidor es de confiaza, y comprueba que no haya extensiones de navegador bloqueando las peticiones.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s eliminó el nombre de la sala.", "Drop File Here": "Deje el fichero aquí", @@ -1357,7 +1357,7 @@ "Compare a unique set of emoji if you don't have a camera on either device": "Comparar un conjunto de iconos si no tienes cámara en ninguno de los dispositivos", "Start": "Empezar", "Waiting for %(displayName)s to verify…": "Esperando la verificación de %(displayName)s…", - "Review": "Revise", + "Review": "Revisar", "in secret storage": "en almacén secreto", "Secret storage public key:": "Clave pública del almacén secreto:", "in account data": "en datos de cuenta", @@ -1544,7 +1544,7 @@ "Theme added!": "¡Se añadió el tema!", "Custom theme URL": "URL de tema personalizado", "Add theme": "Añadir tema", - "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "Para informar de un problema de seguridad relacionado con Matrix, por favor lea Security Disclosure Policy de Matrix.or.", + "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "Para informar de un problema de seguridad relacionado con Matrix, lee la Política de divulgación de seguridad de Matrix.org.", "Keyboard Shortcuts": "Atajos de teclado", "Customise your experience with experimental labs features. Learn more.": "Personaliza tu experiencia con funciones experimentales. Más información.", "Something went wrong. Please try again or view your console for hints.": "Algo salió mal. Por favor, inténtalo de nuevo o mira tu consola para encontrar pistas.", @@ -2102,7 +2102,7 @@ "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Prototipo de comunidades v2. Requiere un servidor compatible. Altamente experimental - usar con precuación.", "Font size": "Tamaño del texto", "Use custom size": "Usar un tamaño personalizado", - "Use a more compact ‘Modern’ layout": "Usar un diseño más «moderno y compacto", + "Use a more compact ‘Modern’ layout": "Usar un diseño más «moderno y compacto»", "Use a system font": "Usar una fuente del sistema", "System font name": "Nombre de la fuente", "Enable experimental, compact IRC style layout": "Activar el diseño experimental de IRC compacto", @@ -2279,8 +2279,8 @@ "Create community": "Crear comunidad", "Failed to find the general chat for this community": "No se pudo encontrar el chat general de esta comunidad", "Security & privacy": "Seguridad y privacidad", - "All settings": "Todos los ajustes", - "Feedback": "Realimentación", + "All settings": "Ajustes", + "Feedback": "Danos tu opinión", "Community settings": "Configuración de la comunidad", "User settings": "Ajustes de usuario", "Switch to light mode": "Cambiar al tema claro", @@ -3248,5 +3248,72 @@ "%(seconds)ss left": "%(seconds)ss restantes", "Failed to send": "No se ha podido mandar", "Change server ACLs": "Cambiar los ACLs del servidor", - "Show options to enable 'Do not disturb' mode": "Mostrar opciones para activar el modo «no molestar»" + "Show options to enable 'Do not disturb' mode": "Mostrar opciones para activar el modo «no molestar»", + "Stop the recording": "Parar grabación", + "Delete recording": "Borrar grabación", + "Enter your Security Phrase a second time to confirm it.": "Escribe tu frase de seguridad de nuevo para confirmarla.", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Elige salas o conversaciones para añadirlas. Este espacio es solo para ti, no informaremos a nadie. Puedes añadir más más tarde.", + "What do you want to organise?": "¿Qué quieres organizar?", + "Filter all spaces": "Filtrar todos los espacios", + "%(count)s results in all spaces|one": "%(count)s resultado en todos los espacios", + "%(count)s results in all spaces|other": "%(count)s resultados en todos los espacios", + "You have no ignored users.": "No has ignorado a nadie.", + "Pause": "Pausar", + "Play": "Reproducir", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Esto es una funcionalidad experimental. Por ahora, los usuarios nuevos que reciban una invitación tendrán que abrirla en para unirse.", + "To view %(spaceName)s, turn on the Spaces beta": "Para ver %(spaceName)s, activa la beta de los espacios", + "To join %(spaceName)s, turn on the Spaces beta": "Para unirte a %(spaceName)s, activa la beta de los espacios", + "Select a room below first": "Selecciona una sala de abajo primero", + "Communities are changing to Spaces": "Las comunidades se van a convertir en espacios", + "Join the beta": "Unirse a la beta", + "Leave the beta": "Salir de la beta", + "Beta": "Beta", + "Tap for more info": "Pulsa para más información", + "Spaces is a beta feature": "Los espacios son una funcionalidad en beta", + "Want to add a new room instead?": "¿Quieres añadir una sala nueva en su lugar?", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Añadiendo salas… (%(progress)s de %(count)s)", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Añadiendo sala…", + "Not all selected were added": "No se han añadido todas las seleccionadas", + "You can add existing spaces to a space.": "Puedes añadir espacios ya existentes dentro de otros espacios.", + "Feeling experimental?": "¿Te animas a probar cosas nuevas?", + "You are not allowed to view this server's rooms list": "No tienes permiso para ver la lista de salas de este servidor", + "Error processing voice message": "Ha ocurrido un error al procesar el mensaje de voz", + "We didn't find a microphone on your device. Please check your settings and try again.": "No hemos encontrado un micrófono en tu dispositivo. Por favor, consulta tus ajustes e inténtalo de nuevo.", + "No microphone found": "No se ha encontrado ningún micrófono", + "We were unable to access your microphone. Please check your browser settings and try again.": "No hemos podido acceder a tu micrófono. Por favor, comprueba los ajustes de tu navegador e inténtalo de nuevo.", + "Unable to access your microphone": "No se ha podido acceder a tu micrófono", + "Your access token gives full access to your account. Do not share it with anyone.": "Tu token de acceso da acceso completo a tu cuenta. No lo compartas con nadie.", + "Access Token": "Token de acceso", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Los espacios son una nueva forma de agrupar salas y personas. Para unirte a uno ya existente, necesitarás que te inviten a él.", + "Please enter a name for the space": "Por favor, elige un nombre para el espacio", + "Connecting": "Conectando", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Puedes salirte de la beta en cualquier momento desde tus ajustes o pulsando sobre la etiqueta de beta, como la que hay arriba.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s se volverá a cargar con los espacios activados. Las comunidades y etiquetas personalizadas se ocultarán.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Versión beta disponible para web, escritorio y Android. Gracias por usar la beta.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s volverá a cargarse con los espacios desactivados. Las comunidades y etiquetas personalizadas serán visibles de nuevo.", + "Spaces are a new way to group rooms and people.": "Los espacios son una nueva manera de agrupar salas y gente.", + "Message search initialisation failed": "Ha fallado la inicialización de la búsqueda de mensajes", + "Spaces are a beta feature.": "Los espacios son una funcionalidad en beta.", + "Search names and descriptions": "Buscar por nombre y descripción", + "You may contact me if you have any follow up questions": "Os podéis poner en contacto conmigo si tenéis alguna pregunta", + "To leave the beta, visit your settings.": "Para salir de la beta, ve a tus ajustes.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Tu nombre de usuario y plataforma serán adjuntados, para que podamos interpretar tus comentarios lo mejor posible.", + "%(featureName)s beta feedback": "Comentarios sobre la funcionalidad beta %(featureName)s", + "Thank you for your feedback, we really appreciate it.": "Muchas gracias por tus comentarios.", + "Beta feedback": "Danos tu opinión sobre la beta", + "Add reaction": "Reaccionar", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "¿Te apetece probar cosas nuevas? Los experimentos son la mejor manera de conseguir acceso anticipado a nuevas funcionalidades, probarlas y ayudar a mejorarlas antes de su lanzamiento. Más información.", + "Send and receive voice messages": "Enviar y recibir mensajes de voz", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Tus comentarios ayudarán a mejorar los espacios. Cuanto más detalle incluyas, mejor.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta disponible para la versión web, de escritorio o Android. Puede que algunas funcionalidades no estén disponibles en tu servidor base.", + "Space Autocomplete": "Autocompletar espacios", + "Go to my space": "Ir a mi espacio", + "sends space invaders": "enviar space invaders", + "Sends the given message with a space themed effect": "Envía un mensaje con efectos espaciales", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Si sales, %(brand)s volverá a cargarse con los espacios desactivados. Las comunidades y las etiquetas personalizadas serán visibles de nuevo.", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Permitir conexión directa (peer-to-peer) en las llamadas individuales (si lo activas, la otra parte podría ver tu dirección IP)", + "See when people join, leave, or are invited to your active room": "Ver cuando alguien se una, salga o se le invite a tu sala activa", + "Kick, ban, or invite people to this room, and make you leave": "Expulsar, vetar o invitar personas a esta sala, y hacerte salir de ella", + "Kick, ban, or invite people to your active room, and make you leave": "Expulsar, vetar o invitar a gente a tu sala activa, o hacerte salir", + "See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala" } diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 56f456b6e4..5e8d744cca 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -652,7 +652,7 @@ "A session's public name is visible to people you communicate with": "Sessiooni avalik nimi on nähtav neile, kellega sa suhtled", "%(brand)s collects anonymous analytics to allow us to improve the application.": "Võimaldamaks meil rakendust parandada kogub %(brand)s anonüümset teavet rakenduse kasutuse kohta.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privaatsus on meile oluline ning seega me ei kogu ei isiklikke ega isikustatavaid andmeid.", - "Learn more about how we use analytics.": "Loe lisaks kuidas me kasutama analüütikat.", + "Learn more about how we use analytics.": "Loe lisaks selles kohta, kuidas me kasutame analüütikat.", "No media permissions": "Meediaõigused puuduvad", "You may need to manually permit %(brand)s to access your microphone/webcam": "Sa võib-olla pead andma %(brand)s'ile loa mikrofoni ja veebikaamera kasutamiseks", "Missing media permissions, click the button below to request.": "Meediaga seotud õigused puuduvad. Nende nõutamiseks klõpsi järgnevat nuppu.", @@ -1202,8 +1202,8 @@ "eg: @bot:* or example.org": "näiteks: @bot:* või example.org", "Subscribed lists": "Tellitud loendid", "Subscribe": "Telli", - "Start automatically after system login": "Käivita automaatselt peale arvutisse sisselogimist", - "Always show the window menu bar": "Näita alati aknas menüüriba", + "Start automatically after system login": "Käivita Element automaatselt peale arvutisse sisselogimist", + "Always show the window menu bar": "Näita aknas alati menüüriba", "Preferences": "Eelistused", "Room list": "Jututubade loend", "Timeline": "Ajajoon", @@ -3286,5 +3286,64 @@ "Including %(commaSeparatedMembers)s": "Sealhulgas %(commaSeparatedMembers)s", "View all %(count)s members|one": "Vaata üht liiget", "View all %(count)s members|other": "Vaata kõiki %(count)s liiget", - "Failed to send": "Saatmine ei õnnestunud" + "Failed to send": "Saatmine ei õnnestunud", + "Enter your Security Phrase a second time to confirm it.": "Kinnitamiseks palun sisesta turvafraas teist korda.", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Lisamiseks vali vestlusi ja jututubasid. Hetkel on see kogukonnakeskus vaid sinu jaoks ja esialgu keegi ei saa sellest teada. Teisi saad liituma kutsuda hiljem.", + "What do you want to organise?": "Mida sa soovid ette võtta?", + "Filter all spaces": "Otsi kõikides kogukonnakeskustest", + "Delete recording": "Kustuta salvestus", + "Stop the recording": "Lõpeta salvestamine", + "%(count)s results in all spaces|one": "%(count)s tulemus kõikides kogukonnakeskustes", + "%(count)s results in all spaces|other": "%(count)s tulemust kõikides kogukonnakeskustes", + "You have no ignored users.": "Sa ei ole veel kedagi eiranud.", + "Play": "Esita", + "Pause": "Peata", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Kas sa tahaksid katsetada? Sa tutvud meie rakenduse uuendustega teistest varem ja võib-olla isegi saad mõjutada arenduse lõpptulemust. Lisateavet liad siit.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "See on katseline funktsionaalsus. Seetõttu uued kutse saanud kasutajad peavad tegelikuks liitumiseks avama kutse siin .", + "To join %(spaceName)s, turn on the Spaces beta": "%(spaceName)s kogukonnakeskusega liitumiseks lülita sisse vastav katseline funktsionaalsus", + "To view %(spaceName)s, turn on the Spaces beta": "%(spaceName)s kogukonnakeskuse vaatamiseks lülita sisse vastav katseline funktsionaalsus", + "Select a room below first": "Esmalt vali alljärgnevast üks jututuba", + "Communities are changing to Spaces": "Seniste kogukondade asemele tulevad kogukonnakeskused", + "Join the beta": "Hakka kasutama beetaversiooni", + "Leave the beta": "Lõpeta beetaversiooni kasutamine", + "Beta": "Beetaversioon", + "Tap for more info": "Lisateabe jaoks klõpsi", + "Spaces is a beta feature": "Kogukonnakeskused on veel katsetamisjärgus funktsionaalsus", + "Want to add a new room instead?": "Kas sa selle asemel soovid lisada jututuba?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Lisan jututuba...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Lisan jututubasid... (%(progress)s/%(count)s)", + "Not all selected were added": "Kõiki valituid me ei lisanud", + "You can add existing spaces to a space.": "Sa võid kogukonnakeskusele lisada ka teisi kogukonnakeskuseid.", + "Feeling experimental?": "Kas sa tahaksid natukene katsetada?", + "You are not allowed to view this server's rooms list": "Sul puuduvad õigused selle serveri jututubade loendi vaatamiseks", + "Error processing voice message": "Viga häälsõnumi töötlemisel", + "We didn't find a microphone on your device. Please check your settings and try again.": "Me ei suutnud sinu seadmest leida mikrofoni. Palun kontrolli seadistusi ja proovi siis uuesti.", + "No microphone found": "Mikrofoni ei leidu", + "We were unable to access your microphone. Please check your browser settings and try again.": "Meil puudub ligipääs sinu mikrofonile. Palun kontrolli oma veebibrauseri seadistusi ja proovi uuesti.", + "Unable to access your microphone": "Puudub ligipääs mikrofonile", + "Your access token gives full access to your account. Do not share it with anyone.": "Sinu pääsuluba annab täismahulise ligipääsu sinu kasutajakontole. Palun ära jaga seda teistega.", + "Access Token": "Pääsuluba", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Kogukonnakeskused on uus viis inimeste ja jututubade ühendamiseks. Kogukonnakeskusega liitumiseks vajad sa kutset.", + "Please enter a name for the space": "Palun sisesta kogukonnakeskuse nimi", + "Connecting": "Kõne on ühendamisel", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Kasuta võrdõigusvõrku 1:1 kõnede jaoks (kui sa P2P-võrgu sisse lülitad, siis teine osapool ilmselt näeb sinu IP-aadressi)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Kõik funtsionaalsused ei pruugi sinu koduserveri poolt olla toetatud.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Sa võid beetaversiooni kasutamise lõpetada niipea, kui tahad. Selleks klõpsi beeta-silti, mida näed siin samas ülal.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "Käivitame %(brand)s uuesti nii, et kogukonnakeskused on kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid on siis välja lülitatud.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Tänud, et oled huviline katsetama meie rakendust.", + "Spaces are a new way to group rooms and people.": "Kogukonnakeskused on uus viis jututubade ja inimeste ühendamiseks.", + "Spaces are a beta feature.": "Kogukonnakeskused on veel katsetamisjärgus funktsionaalsus.", + "Search names and descriptions": "Otsi nimede ja kirjelduste seast", + "You may contact me if you have any follow up questions": "Kui sul on lisaküsimusi, siis vastan neile hea meelega", + "To leave the beta, visit your settings.": "Beetaversiooni saad välja lülitada rakenduse seadistustest.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Lisame sinu kommentaaridele ka kasutajanime ja operatsioonisüsteemi.", + "%(featureName)s beta feedback": "%(featureName)s testversiooni tagasiside", + "Thank you for your feedback, we really appreciate it.": "Täname sind nende kommentaaride eest.", + "Beta feedback": "Tagasiside testversioonile", + "Add reaction": "Lisa reaktsioon", + "Send and receive voice messages": "Saada ja võta vastu häälsõnumeid", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Sinu tagasiside aitab teha kogukonnakeskuseid paremaks. Mida detailsemalt sa oma arvamust kirjeldad, seda parem.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Kui sa lahkud, siis käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.", + "Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud" } diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 5948048561..46dde79945 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -73,7 +73,7 @@ "(HTTP status %(httpStatus)s)": "(HTTP وضعیت %(httpStatus)s)", "Failed to forget room %(errCode)s": "فراموش کردن اتاق با خطا مواجه شد %(errCode)s", "Wednesday": "چهارشنبه", - "Quote": "گفتآورد", + "Quote": "نقل قول", "Send": "ارسال", "Error": "خطا", "Send logs": "ارسال گزارش‌ها", @@ -355,7 +355,7 @@ "You are not in this room.": "شما در این اتاق نیستید.", "Power level must be positive integer.": "سطح قدرت باید عدد صحیح مثبت باشد.", "This room is not recognised.": "این اتاق شناخته نشده است.", - "Missing roomId.": "شناسه‌ی اتاق گم‌شده", + "Missing roomId.": "شناسه‌ی اتاق گم‌شده.", "Unable to create widget.": "ایجاد ابزارک امکان پذیر نیست.", "You need to be able to invite users to do that.": "نیاز است که شما قادر به دعوت کاربران به آن باشید.", "You need to be logged in.": "شما باید وارد شوید.", @@ -632,5 +632,2380 @@ "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "هرگاه این صفحه شامل اطلاعات قابل شناسایی مانند شناسه‌ی اتاق ، کاربر یا گروه باشد ، این داده‌ها قبل از ارسال به سرور حذف می شوند.", "Your user agent": "نماینده کاربری شما", "Whether you're using %(brand)s as an installed Progressive Web App": "این که آیا شما از%(brand)s به عنوان یک PWA استفاده می‌کنید یا نه", - "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "این که آیا از ویژگی 'breadcrumbs' (نمایه‌ی کاربری بالای فهرست اتاق‌ها) استفاده می‌کنید یا خیر" + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "این که آیا از ویژگی 'breadcrumbs' (نمایه‌ی کاربری بالای فهرست اتاق‌ها) استفاده می‌کنید یا خیر", + "Use an identity server to invite by email. Manage in Settings.": "برای دعوت از یک سرور هویت‌سنجی استفاده نمائید. می‌توانید این مورد را در تنظیمات پیکربندی نمائید.", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "برای دعوت با استفاده از ایمیل از یک سرور هویت‌سنجی استفاده نمائید. جهت استفاده از سرور هویت‌سنجی پیش‌فرض (%(defaultIdentityServerName)s) بر روی ادامه کلیک کنید، وگرنه آن را در بخش تنظیمات پیکربندی نمائید.", + "Joins room with given address": "به اتاق با آدرس داده‌شده بپیوندید", + "WARNING: Session already verified, but keys do NOT MATCH!": "هشدار امنیتی: نشست پیش از این تائید شده، اما کلیدها مطابقت ندارد!", + "Session already verified!": "نشست پیش از این تائید شده‌است!", + "Unknown (user, session) pair:": "جفت (کاربر، نشست) ناشناخته:", + "Verifies a user, session, and pubkey tuple": "یک کاربر، نشست و عبارت کلید عمومی را تائید می‌کند", + "You cannot modify widgets in this room.": "شما امکان تغییر ویجت‌ها در این اتاق را ندارید.", + "Please supply a https:// or http:// widget URL": "لطفا نشانی یک ویجت را به پروتکل http:// یا https:// وارد کنید", + "Please supply a widget URL or embed code": "لطفا نشانی (URL) ویجت یا یک کد قابل جاسازی (embeded) وارد کنید", + "Adds a custom widget by URL to the room": "یک ویجت سفارشی را با استفاده از نشانی (URL) به اتاق اضافه می‌کند", + "Opens the Developer Tools dialog": "پنجره‌ی ابزار توسعه را باز می‌کند", + "Could not find user in room": "کاربر در اتاق یافت نشد", + "Command failed": "دستور موفقیت‌آمیز نبود", + "Define the power level of a user": "سطح قدرت یک کاربر را تعریف کنید", + "You are no longer ignoring %(userId)s": "شما دیگر کاربر %(userId)s را نادیده نمی‌گیرید", + "Unignored user": "کاربران نادیده گرفته‌نشده", + "Stops ignoring a user, showing their messages going forward": "توقف نادیده گرفتن یک کاربر، باعث می‌شود پیام‌های او به شما نمایش داده شود", + "You are now ignoring %(userId)s": "شما هم‌اکنون کاربر %(userId)s را نادیده گرفتید", + "Ignored user": "کاربران نادیده گرفته‌شده", + "Ignores a user, hiding their messages from you": "نادیده گرفتن یک کاربر، باعث می‌شود پیام‌های او به شما نمایش داده نشود", + "Unbans user with given ID": "رفع تحریم کاربر با شناسه‌ی مذکور", + "Bans user with given id": "تحریم کاربر با شناسه‌ی مذکور", + "Kicks user with given id": "اخراج کاربر با شناسه‌ی مذکور", + "Unrecognised room address:": "آدرس اتاق قابل تشخیص نیست:", + "Leave room": "ترک اتاق", + "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "کلید امضای ارائه شده با کلید امضای دریافت شده از جلسه %(deviceId)s کاربر %(userId)s مطابقت دارد. نشست به عنوان تأیید شده علامت گذاری شد.", + "Verified key": "کلید تأیید شده", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "هشدار: تایید کلید ناموفق بود! کلید امضا کننده %(userId)s در نشست %(deviceId)s برابر %(fprint)s است که با کلید %(fingerprint)s تطابق ندارد. این می تواند به معنی رهگیری ارتباطات شما باشد!", + "Send a bug report with logs": "گزارش یک اشکال به همراه سیاهه‌های مربوط", + "Displays information about a user": "اطلاعات مربوط به کاربر را نمایش می دهد", + "Displays list of commands with usages and descriptions": "لیست دستورات را با کاربردها و توضیحات نمایش می دهد", + "Sends the given message coloured as a rainbow": "پیام داده شده را به صورت رنگین کمان ارسال می کند", + "Forces the current outbound group session in an encrypted room to be discarded": "جلسه گروه خروجی فعلی را در یک اتاق رمزگذاری شده مجبور می کند که کنار گذاشته شود", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s دعوتنامه %(displayName)s را پذیرفت.", + "Reason": "دلیل", + "Displays action": "عملکرد را نمایش می دهد", + "Places the call in the current room on hold": "تماس را در اتاق فعلی در حالت تعلیق قرار می دهد", + "Sends a message to the given user": "برای کاربر داده شده پیامی ارسال می کند", + "Opens chat with the given user": "گپ با کاربر داده شده را باز می کند", + "%(targetName)s accepted an invitation.": "%(targetName)s دعوتنامه را پذیرفت.", + "%(senderName)s invited %(targetName)s.": "%(senderName)s %(targetName)s را دعوت کرد.", + "%(senderName)s banned %(targetName)s.": "%(senderName)s %(targetName)s را ممنوع کرد.", + "See when anyone posts a sticker to your active room": "ببینید چه وقتی برچسب به اتاق فعال شما ارسال می شود", + "Send stickers to your active room as you": "همانطور که هستید ، برچسب ها را به اتاق فعال خود ارسال کنید", + "See when a sticker is posted in this room": "زمان نصب برچسب در این اتاق را ببینید", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s این اتاق را ارتقا داد.", + "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s نام اتاق را به %(roomName)s تغییر داد.", + "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s نام اتاق را از %(oldRoomName)s به %(newRoomName)s تغییر داد.", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s موضوع را به %(topic)s تغییر داد.", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s %(targetName)s را اخراج کرد.", + "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s دعوت %(targetName)s را پس گرفت.", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s از %(targetName)s رفع مسدودیت کرد.", + "%(targetName)s left the room.": "%(targetName)s اتاق را ترک کرد.", + "%(targetName)s rejected the invitation.": "%(targetName)s دعوت را رد کرد.", + "%(targetName)s joined the room.": "%(targetName)s به اتاق پیوست.", + "%(senderName)s made no change.": "%(senderName)s تغییری نداد.", + "%(senderName)s set a profile picture.": "%(senderName)s عکس نمایه ای تنظیم کرد.", + "%(senderName)s removed their profile picture.": "%(senderName)s عکس نمایه خود را حذف کرد.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s نام نمایشی خود %(oldDisplayName)s را حذف کرد.", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s نام نمایشی خود را به %(displayName)s تنظیم کرد.", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s نام نمایشی خود را به %(displayName)s تغییر داد.", + "Repeats like \"aaa\" are easy to guess": "تکرارهایی مانند بببب به راحتی قابل حدس هستند", + "with an empty state key": "با یک کلید حالت خالی", + "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 شرکت همه سرورها ممنوع است! دیگر نمی توان از این اتاق استفاده کرد.", + "Converts the DM to a room": "DM را به اتاق تبدیل می کند", + "Converts the room to a DM": "اتاق را به DM تبدیل می کند", + "Takes the call in the current room off hold": "تماس را در اتاق فعلی خاموش نگه می دارد", + "Sends the given emote coloured as a rainbow": "emote داده شده را به صورت رنگین کمان می فرستد", + "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s ACL های سرور را برای این اتاق تغییر داد.", + "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s ACL های سرور را برای این اتاق تنظیم کرده است.", + "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s دسترسی مهمانان را به %(rule)s تغییر داد", + "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s از پیوستن مهمان به اتاق جلوگیری کرد.", + "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s به مهمانان اجازه عضویت در اتاق را داد.", + "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s قانون عضویت را به %(rule)s تغییر داد", + "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s این اتاق را مخصوص دعوت شدگان قرار داد.", + "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s اتاق را برای هر کسی که پیوند را می داند عمومی کرد.", + "See when people join, leave, or are invited to this room": "ببینید که کی مردم در این اتاق عضو شده اند، ترک کرده اند یا به آن دعوت شده اند", + "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s flair را برای %(groups)s در این اتاق غیر فعال کرد.", + "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s flair را برای گروه %(groups)s در این اتاق فعال کرد.", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s پیام های پین شده را برای اتاق تغییر داد.", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s سطح قدرت %(powerLevelDiffText)s تغییر داد.", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s از %(fromPowerLevel)s به %(toPowerLevel)s", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s تاریخچه از این به بعد این اتاق را به وضعیت ناشناخته %(visibility)s تغییر داد.", + "%(senderName)s made future room history visible to anyone.": "%(senderName)s تاریخچه از بعد این اتاق را برای همه قابل مشاهده کرد.", + "%(senderName)s made future room history visible to all room members.": "%(senderName)s تاریخچه اتاق را برای همه اعضای اتاق قابل مشاهده کرده است.", + "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s تاریخچه اتاق آینده را از همان نقطه ای که به آن پیوسته اند ، برای همه اعضای اتاق قابل مشاهده کرد.", + "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s تاریخچه اتاق آینده را از همان جایی که دعوت شده اند برای همه اعضای اتاق قابل مشاهده کرد.", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s %(targetDisplayName)s را به اتاق دعوت کرد.", + "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s دعوت نامه %(targetDisplayName)s را برای پیوستن به اتاق باطل کرد.", + "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s تماس تصویری برقرار کرد. (توسط این مرورگر پشتیبانی نمی شود)", + "%(senderName)s placed a video call.": "%(senderName)s تماس تصویری برقرار کرد.", + "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s تماس صوتی برقرار کرد. (توسط این مرورگر پشتیبانی نمی شود)", + "%(senderName)s placed a voice call.": "%(senderName)s تماس صوتی برقرار کرد.", + "%(senderName)s declined the call.": "%(senderName)s تماس را رد کرد.", + "(unknown failure: %(reason)s)": "(خطای ناشناخته: %(reason)s)", + "%(senderName)s changed the addresses for this room.": "%(senderName)s آدرس های این اتاق را تغییر داد.", + "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s آدرس اصلی و جایگزین این اتاق را تغییر داد.", + "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s آدرس های جایگزین این اتاق را تغییر داد.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s آدرس جایگزین %(addresses)s این اتاق را حذف کرد.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s آدرس های جایگزین %(addresses)s این اتاق را حذف کرد.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s آدرس جایگزین %(addresses)s را برای این اتاق اضافه کرد.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s آدرس های جایگزین %(addresses)s را برای این اتاق اضافه کرد.", + "%(senderName)s removed the main address for this room.": "%(senderName)s آدرس اصلی این اتاق را حذف کرد.", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s آدرس اصلی این اتاق را روی %(address)s تنظیم کرد.", + "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s تصویری ارسال کرد.", + "(no answer)": "(بدون پاسخ)", + "(an error occurred)": "(خطایی رخ داده است)", + "(their device couldn't start the camera / microphone)": "(دستگاه آنها نمی تواند دوربین / میکروفون را راه اندازی کند)", + "(connection failed)": "(ارتباط ناموفق بود)", + "(could not connect media)": "(امکان اتصال رسانه وجود ندارد)", + "(not supported by this browser)": "(توسط این مرورگر پشتیبانی نمی شود)", + "Someone": "کسی", + "Your %(brand)s is misconfigured": "%(brand)s‌ی شما به درستی پیکربندی نشده‌است", + "Ensure you have a stable internet connection, or get in touch with the server admin": "از اتصال اینترنت پایدار اطمینان حاصل‌کرده و سپس با مدیر سرور ارتباط بگیرید", + "Cannot reach homeserver": "دسترسی به سرور میسر نیست", + "See %(msgtype)s messages posted to your active room": "پیام های %(msgtype)s ارسال شده به اتاق فعال خودتان را مشاهده کنید", + "See %(msgtype)s messages posted to this room": "پیام های %(msgtype)s ارسال شده به این اتاق را مشاهده کنید", + "Send %(msgtype)s messages as you in your active room": "همانطور که در اتاق فعال خودتان هستید پیام های %(msgtype)s را ارسال کنید", + "Send %(msgtype)s messages as you in this room": "همانطور که در این اتاق هستید پیام های %(msgtype)s را ارسال کنید", + "See general files posted to your active room": "فایل‌های ارسال شده در اتاق فعال خودتان را مشاهده کنید", + "See general files posted to this room": "فایل‌های ارسال شده در این اتاق را مشاهده کنید", + "Send general files as you in your active room": "همانطور که در اتاق فعال خود هستید فایل ارسال کنید", + "Send general files as you in this room": "همانطور که در این اتاق هستید فایل ارسال کنید", + "See videos posted to your active room": "فیلم های ارسال شده در اتاق فعال خودتان را مشاهده کنید", + "See videos posted to this room": "فیلم های ارسال شده در این اتاق را مشاهده کنید", + "Send videos as you in your active room": "همانطور که در اتاق فعال خود هستید فیلم ارسال کنید", + "Send videos as you in this room": "همانطور که در این اتاق هستید فیلم ارسال کنید", + "See images posted to your active room": "تصاویری را که در اتاق فعال خودتان ارسال شده‌اند، مشاهده کنید", + "See images posted to this room": "تصاویری را که در این اتاق ارسال شده‌اند، مشاهده کنید", + "Send images as you in your active room": "همانطور که در اتاق فعال خود هستید تصاویر را ارسال کنید", + "Send images as you in this room": "همانطور که در این اتاق هستید تصاویر را ارسال کنید", + "Send emotes as you in your active room": "همانطور که در اتاق فعال خود هستید شکلک‌های خود را ارسال کنید", + "Send emotes as you in this room": "همانطور که در این اتاق هستید شکلک‌های خود را ارسال کنید", + "Send text messages as you in your active room": "همانطور که در اتاق فعال خود هستید پیام های متنی ارسال کنید", + "Send text messages as you in this room": "همانطور که در این اتاق هستید پیام های متنی ارسال کنید", + "Send messages as you in your active room": "همانطور که در اتاق فعال خود هستید پیام ارسال کنید", + "Send messages as you in this room": "همانطور که در این اتاق هستید پیام ارسال کنید", + "See emotes posted to your active room": "شکلک‌های ارسال‌شده در اتاق فعال خودتان را مشاهده کنید", + "See emotes posted to this room": "شکلک‌های ارسال‌شده در این اتاق را مشاهده کنید", + "See text messages posted to your active room": "پیام‌های متنی که در اتاق فعال شما ارسال شده‌اند را مشاهده کنید", + "See text messages posted to this room": "پیام‌های متنی که در این گروه ارسال شده‌اند را مشاهده کنید", + "See messages posted to your active room": "مشاهده‌ی پیام‌هایی که در اتاق فعال شما ارسال شده‌اند", + "See messages posted to this room": "مشاهده‌ی پیام‌هایی که در این اتاق ارسال شده‌اند", + "The %(capability)s capability": "قابلیت %(capability)s", + "See %(eventType)s events posted to your active room": "رخدادهای %(eventType)s را که در اتاق فعال شما ارسال شده، مشاهده کنید", + "See %(eventType)s events posted to this room": "رخداد‌های %(eventType)s را که در این اتاق ارسال شده‌اند مشاهده کنید", + "with state key %(stateKey)s": "با کلید وضعیت (state key) %(stateKey)s", + "Send stickers to this room as you": "با مشخصات کاربری خودتان در این گروه استیکر ارسال نمائید", + "See when people join, leave, or are invited to your active room": "هنگامی که افراد به اتاق فعال شما دعوت می‌شوند، آن را ترک می‌کنند و لیست افراد دعوت شده به آن را مشاهده کنید", + "See when the avatar changes in your active room": "تغییرات نمایه‌ی اتاق فعال خود را مشاهده کنید", + "Change the avatar of your active room": "نمایه‌ی اتاق فعال خود را تغییر دهید", + "See when the avatar changes in this room": "تغییرات نمایه‌ی این اتاق را مشاهده کنید", + "Change the avatar of this room": "نمایه‌ی این اتاق را تغییر دهید", + "See when the name changes in your active room": "تغییرات نام اتاق فعال خود را مشاهده کنید", + "Change the name of your active room": "نام اتاق فعال خود را تغییر دهید", + "See when the name changes in this room": "تغییرات نام این اتاق را مشاهده کنید", + "Change the name of this room": "نام این اتاق را تغییر دهید", + "See when the topic changes in your active room": "تغییرات عنوان را در اتاق فعال خود مشاهده کنید", + "Change the topic of your active room": "عنوان اتاق فعال خود را تغییر دهید", + "See when the topic changes in this room": "تغییرات عنوان این اتاق را مشاهده کنید", + "Change the topic of this room": "عنوان این اتاق را تغییر دهید", + "Change which room, message, or user you're viewing": "اتاق، پیام و کاربرانی را که مشاهده می‌کنید، تغییر دهید", + "Change which room you're viewing": "اتاق‌هایی را که مشاهده می‌کنید تغییر دهید", + "Send stickers into your active room": "در اتاق‌های فعال خود استیکر ارسال کنید", + "Send stickers into this room": "در این اتاق استیکر ارسال کنید", + "%(names)s and %(lastPerson)s are typing …": "%(names)s و %(lastPerson)s در حال نوشتن…", + "%(names)s and %(count)s others are typing …|one": "%(names)s و یک نفر دیگر در حال نوشتن…", + "%(names)s and %(count)s others are typing …|other": "%(names)s و %(count)s نفر دیگر در حال نوشتن…", + "%(displayName)s is typing …": "%(displayName)s در حال نوشتن…", + "Dark": "تاریک", + "Light": "روشن", + "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s یک قاعده تحریم را که با %(oldGlob)s تطابق داشت، به دلیل (دلایل) %(reason)s به گونه‌ای به‌روزرسانی کرد که با %(newGlob)s تطابق داشته باشد", + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s یک قاعده تحریم سرورها را که با %(oldGlob)s تطابق داشت، به دلیل (دلایل) %(reason)s به گونه‌ای تغییر داد که با %(newGlob)s تطابق داشته باشد", + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s یک قاعده تحریم اتاق‌ها را که با %(oldGlob)s تطابق داشت، به دلیل (دلایل) %(reason)s به گونه‌ای تغییر داد که با %(newGlob)s تطابق داشته باشد", + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s یک قاعده تحریم کاربران را که با %(oldGlob)s تطابق داشت، به دلیل (دلایل) %(reason)s به گونه‌ای تغییر داد که با %(newGlob)s تطابق داشته باشد", + "%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s یک قاعده تحریم را که با %(glob)s تطابق دارد، به دلیل (دلایل) %(reason)s ایجاد کرد", + "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s یک قاعده تحریم سرورها را که با %(glob)s تطابق دارد، به دلیل (دلایل) %(reason)s ایجاد کرد", + "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s یک قاعده تحریم اتاق‌ها را که با %(glob)s تطابق دارد، به دلیل (دلایل) %(reason)s ایجاد کرد", + "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s یک قاعده تحریم کاربران را که با %(glob)s تطابق دارد، به دلیل (دلایل) %(reason)s ایجاد کرد", + "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s یک قاعده تحریم را که با %(glob)s تطابق داشت، به دلیل (دلایل) %(reason)s به‌روزرسانی کرد", + "%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s قاعده تحریم سرورها را که با %(glob)s تطابق داشت، به دلیل (دلایل) %(reason)s به‌روزرسانی کرد", + "%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s قاعده تحریم اتاق‌ها را که با %(glob)s تطابق داشت، به دلیل (دلایل) %(reason)s به‌روزرسانی کرد", + "%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s قاعده تحریم کاربران را که با %(glob)s تطابق داشت، به دلیل (دلایل) %(reason)s به‌روزرسانی کرد", + "%(senderName)s updated an invalid ban rule": "%(senderName)s یک قاعده‌ی تحریم نامعتبر را به‌روزرسانی کرد", + "%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s قاعده تحریمی را که با %(glob)s تطابق داشت، حذف کرد", + "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s قاعده تحریم سرورها را که با %(glob)s تطابق داشت، حذف کرد", + "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s قاعده تحریم اتاق‌ها را که با %(glob)s تطابق داشت، حذف کرد", + "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s قاعده تحریم کاربران را که با %(glob)s تطابق داشت، حذف کرد", + "%(senderName)s has updated the widget layout": "%(senderName)s چیدمان ویجت را به‌روز کرد", + "%(widgetName)s widget removed by %(senderName)s": "ویجت %(widgetName)s توسط %(senderName)s حذف گردید", + "%(widgetName)s widget added by %(senderName)s": "ویجت %(widgetName)s توسط %(senderName)s اضافه شد", + "%(widgetName)s widget modified by %(senderName)s": "ویجت %(widgetName)s توسط %(senderName)s تغییر کرد", + "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s قابلیت flair را در این اتاق برای %(newGroups)s فعال و برای %(oldGroups)s غیر فعال کرد.", + "The server has denied your request.": "سرور درخواست شما را رد کرده است.", + "Use your Security Key to continue.": "برای ادامه از کلید امنیتی خود استفاده کنید.", + "This session, or the other session": "این نشست، یا نشست دیگر", + "%(creator)s created and configured the room.": "%(creator)s اتاق را ایجاد و پیکربندی کرد.", + "Report Content to Your Homeserver Administrator": "گزارش محتوا به مدیر سرور خود", + "Be found by phone or email": "از طریق تلفن یا ایمیل پیدا شوید", + "Find others by phone or email": "دیگران را از طریق تلفن یا ایمیل پیدا کنید", + "Sign out and remove encryption keys?": "خروج از حساب کاربری و حذف کلیدهای رمزنگاری؟", + "Remember my selection for this widget": "انتخاب من برای این ابزارک را بخاطر بسپار", + "I don't want my encrypted messages": "پیام‌های رمزشده‌ی خود را نمی‌خواهم", + "Upgrade this room to version %(version)s": "این اتاق را به نسخه %(version)s ارتقا دهید", + "Please enter the code it contains:": "لطفا کدی را که در آن وجود دارد وارد کنید:", + "Failed to save space settings.": "تنظیمات فضای کاری ذخیره نشد.", + "Not a valid Security Key": "کلید امنیتی معتبری نیست", + "This widget would like to:": "این ابزارک تمایل دارد:", + "Unable to set up keys": "تنظیم کلیدها امکان پذیر نیست", + "%(completed)s of %(total)s keys restored": "%(completed)s از %(total)s کلید بازیابی شدند", + "a new cross-signing key signature": "یک کلید امضای متقابل جدید", + "a new master key signature": "یک شاه‌کلید جدید", + "Close dialog or context menu": "بستن پنجره یا منو", + "Your account is not secure": "حساب کاربری شما امن نیست", + "Please fill why you're reporting.": "لطفا توضیح دهید که چرا گزارش می‌دهید.", + "Password is allowed, but unsafe": "گذرواژه مجاز است ، اما ناامن است", + "Upload files (%(current)s of %(total)s)": "بارگذاری فایل‌ها (%(current)s از %(total)s)", + "Failed to decrypt %(failedCount)s sessions!": "رمزگشایی %(failedCount)s نشست موفقیت‌آمیز نبود!", + "Unable to load backup status": "بارگیری و نمایش وضعیت نسخه‌ی پشتیبان امکان‌پذیر نیست", + "Link to most recent message": "پیوند به آخرین پیام", + "Clear Storage and Sign Out": "فضای ذخیره‌سازی را پاک کرده و از حساب کاربری خارج شوید", + "Error whilst fetching joined communities": "هنگام واکشی اجتماع‌هایی که عضو آن‌ها هستید، خطایی رخ داد", + "Make this space private": "این فضای کاری را خصوصی کن", + "Unable to validate homeserver": "تأیید اعتبار سرور امکان‌پذیر نیست", + "Sign into your homeserver": "وارد سرور خود شوید", + "%(creator)s created this DM.": "%(creator)s این گفتگو را ایجاد کرد.", + "You’re all caught up": "همه‌ی کارها را انجام دادید", + "The server is offline.": "سرور آفلاین است.", + "You're all caught up.": "همه‌ی کارها را انجام دادید.", + "Successfully restored %(sessionCount)s keys": "کلیدهای %(sessionCount)s با موفقیت بازیابی شدند", + "Fetching keys from server...": "واکشی کلیدها از سرور ...", + "Restoring keys from backup": "بازیابی کلیدها از نسخه پشتیبان", + "Interactively verify by Emoji": "تأیید تعاملی با استفاده از شکلک", + "Manually Verify by Text": "تائید دستی با استفاده از متن", + "a device cross-signing signature": "کلید امضای متقابل یک دستگاه", + "Upload %(count)s other files|one": "بارگذاری %(count)s فایل دیگر", + "Upload %(count)s other files|other": "بارگذاری %(count)s فایل دیگر", + "Room Settings - %(roomName)s": "تنظیمات اتاق - %(roomName)s", + "Start using Key Backup": "شروع استفاده از نسخه‌ی پشتیبان کلید", + "Unable to restore backup": "بازیابی نسخه پشتیبان امکان پذیر نیست", + "Clear cache and resync": "پاک کردن حافظه‌ی کش و همگام سازی مجدد", + "Failed to upgrade room": "اتاق ارتقاء نیافت", + "Link to selected message": "پیوند به پیام انتخاب شده", + "Failed to load %(groupId)s": "بارگیری و نمایش %(groupId)s انجام نشد", + "Community %(groupId)s not found": "اجتماع %(groupId)s یافت نشد", + "Unable to restore session": "امکان بازیابی نشست وجود ندارد", + "Verify other login": "ورود دیگر را تائید کنید", + "Reset event store": "پاک‌کردن مخزن رخداد", + "Reset event store?": "پاک‌کردن مخزن رخداد؟", + "%(count)s messages deleted.|one": "%(count)s پیام پاک شد.", + "%(count)s messages deleted.|other": "%(count)s پیام پاک شد.", + "Invite to %(roomName)s": "دعوت به %(roomName)s", + "View dev tools": "مشاهده ابزارهای توسعه", + "Upgrade to %(hostSignupBrand)s": "ارتقاء به %(hostSignupBrand)s", + "Enter Security Key": "کلید امنیتی را وارد کنید", + "Enter Security Phrase": "عبارت امنیتی را وارد کنید", + "Incorrect Security Phrase": "عبارت امنیتی نادرست است", + "Security Key mismatch": "عدم تطابق کلید امنیتی", + "Invalid Security Key": "کلید امنیتی نامعتبر است", + "Wrong Security Key": "کلید امنیتی اشتباه است", + "Specify a homeserver": "یک سرور مشخص کنید", + "Continuing without email": "ادامه بدون ایمیل", + "Approve widget permissions": "دسترسی‌های ابزارک را تائید کنید", + "Server isn't responding": "سرور پاسخ نمی دهد", + "Unable to upload": "بارگذاری امکان پذیر نیست", + "Signature upload failed": "بارگذاری امضا انجام نشد", + "Signature upload success": "موفقیت در بارگذاری امضا", + "Cancelled signature upload": "بارگذاری امضا لغو شد", + "a key signature": "یک امضای کلیدی", + "Clear cross-signing keys": "کلیدهای امضای متقابل را پاک کن", + "Destroy cross-signing keys?": "کلیدهای امضای متقابل نابود شود؟", + "This wasn't me": "این من نبودم", + "Recently Direct Messaged": "گفتگوهای خصوصی اخیر", + "Upgrade public room": "ارتقاء اتاق عمومی", + "Upgrade private room": "ارتقاء اتاق خصوصی", + "Automatically invite users": "به طور خودکار کاربران را دعوت کن", + "Resend %(unsentCount)s reaction(s)": "بازارسال %(unsentCount)s واکنش", + "Missing session data": "داده‌های نشست از دست رفته است", + "Manually export keys": "کلیدها را به صورت دستی استخراج (Export)کن", + "No backup found!": "نسخه پشتیبان یافت نشد!", + "Incompatible local cache": "حافظه‌ی محلی ناسازگار", + "Upgrade Room Version": "ارتقاء نسخه‌ی اتاق", + "Share Room Message": "به اشتراک گذاشتن پیام اتاق", + "Collapse Reply Thread": "جمع کردن ریسه‌ی پاسخ", + "Reset everything": "همه چیز را بازراه‌اندازی (reset) کنید", + "Consult first": "ابتدا مشورت کنید", + "Save Changes": "ذخیره تغییرات", + "Leave Space": "ترک فضای کاری", + "Space settings": "تنظیمات فضای کاری", + "Unnamed Space": "فضای کاری بدون نام", + "Remember this": "این را به یاد داشته باش", + "Invalid URL": "آدرس URL نامعتبر", + "About homeservers": "درباره سرورها", + "Learn more": "بیشتر بدانید", + "Other homeserver": "سرور دیگر", + "Decline All": "رد کردن همه", + "Modal Widget": "ابزارک کمکی", + "Updating %(brand)s": "به‌روزرسانی %(brand)s", + "Looks good!": "به نظر خوب میاد!", + "Security Key": "کلید امنیتی", + "Security Phrase": "عبارت امنیتی", + "Keys restored": "کلیدها بازیابی شدند", + "Upload completed": "بارگذاری انجام شد", + "Your password": "گذرواژه شما", + "Not Trusted": "قابل اعتماد نیست", + "Session key": "کلید نشست", + "Session name": "نام نشست", + "Verify session": "تائید نشست", + "New session": "نشست جدید", + "Country Dropdown": "لیست کشور", + "Verification Request": "درخواست تأیید", + "Send report": "ارسال گزارش", + "Integration Manager": "مدیر یکپارچه‌سازی", + "Command Help": "راهنمای دستور", + "Message edits": "ویرایش پیام", + "Upload all": "بارگذاری همه", + "Upload Error": "خطای بارگذاری", + "Cancel All": "لغو همه", + "Upload files": "بارگذاری فایل‌ها", + "Phone (optional)": "شماره تلفن (اختیاری)", + "Email (optional)": "ایمیل (اختیاری)", + "Share Community": "به اشتراک‌گذاری اجتماع", + "Share User": "به اشتراک‌گذاری کاربر", + "Share Room": "به اشتراک‌گذاری اتاق", + "Send Logs": "ارسال گزارش ها", + "Suggestions": "پیشنهادات", + "Recent Conversations": "گفتگوهای اخیر", + "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "این کاربران ممکن است وجود نداشته یا نامعتبر باشند و نمی‌توان آنها را دعوت کرد: %(csvNames)s", + "Failed to find the following users": "این کاربران یافت نشدند", + "Failed to transfer call": "انتقال تماس انجام نشد", + "A call can only be transferred to a single user.": "تماس فقط می تواند به یک کاربر منتقل شود.", + "We couldn't invite those users. Please check the users you want to invite and try again.": "ما نتوانستیم آن کاربران را دعوت کنیم. لطفاً کاربرانی را که می خواهید دعوت کنید بررسی کرده و دوباره امتحان کنید.", + "Something went wrong trying to invite the users.": "در تلاش برای دعوت از کاربران مشکلی پیش آمد.", + "We couldn't create your DM.": "نتوانستیم گفتگوی خصوصی مد نظرتان را ایجاد کنیم.", + "Failed to invite the following users to chat: %(csvUsers)s": "دعوت از این کاربران برای شروع گفتگو موفقیت‌آمیز نبود: %(csvUsers)s", + "Invite by email": "دعوت از طریق ایمیل", + "Click the button below to confirm your identity.": "برای تأیید هویت خود بر روی دکمه زیر کلیک کنید.", + "Confirm to continue": "برای ادامه تأیید کنید", + "To continue, use Single Sign On to prove your identity.": "برای ادامه از احراز هویت یکپارچه جهت اثبات هویت خود استفاده نمائید.", + "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "%(brand)s شما اجازه استفاده از سیستم مدیریت ادغام را برای این کار نمی دهد. لطفا با ادمین تماس بگیرید.", + "Integrations not allowed": "یکپارچه‌سازی‌ها اجازه داده نشده‌اند", + "Enable 'Manage Integrations' in Settings to do this.": "برای انجام این کار 'مدیریت پکپارچه‌سازی‌ها' را در تنظیمات فعال نمائید.", + "Integrations are disabled": "پکپارچه‌سازی‌ها غیر فعال هستند", + "Incoming Verification Request": "درخواست تأیید دریافتی", + "Waiting for partner to confirm...": "منتظر تائید طرف مقابل...", + "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "با تأیید این دستگاه، آن را به عنوان مورد اعتماد علامت‌گذاری کرده و کاربرانی که شما را تأیید کرده اند، به این دستگاه اعتماد خواهند کرد.", + "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.": "این دستگاه را تأیید کنید تا به عنوان مورد اعتماد علامت‌گذاری شود. اعتماد به این دستگاه در هنگام استفاده از رمزنگاری سرتاسر آرامش و اطمینان بیشتری را برای شما به ارمغان می‌آورد.", + "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "با تأیید این کاربر ، نشست وی به عنوان مورد اعتماد علامت‌گذاری شده و همچنین نشست شما به عنوان مورد اعتماد برای وی علامت‌گذاری خواهد شد.", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "این کاربر را تأیید کنید تا به عنوان کاربر مورد اعتماد علامت‌گذاری شود. اعتماد به کاربران آرامش و اطمینان بیشتری به شما در استفاده از رمزنگاری سرتاسر می‌دهد.", + "Minimize dialog": "کوچک‌کردن پنجره", + "Maximize dialog": "بزرگ‌کردن پنجره", + "%(hostSignupBrand)s Setup": "راه‌اندازی %(hostSignupBrand)s", + "You should know": "باید بدانید", + "Terms of Service": "شرایط استفاده از خدمات", + "Privacy Policy": "سیاست حفظ حریم خصوصی", + "Cookie Policy": "سیاست کوکی", + "Learn more in our , and .": "درباره‌ی ، و بیشتر بدانید.", + "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "ادامه‌ی موقت به فرآیند راه‌اندازی%(hostSignupBrand)s اجازه می‌دهد به حساب کاربری شما برای تائید آدرس‌های ایمیلتان دسترسی داشته باشد. این داده‌ها ذخیره نمی‌شوند.", + "Failed to connect to your homeserver. Please close this dialog and try again.": "اتصال به سرور شما انجام نشد. لطفاً این پنجره را بسته و دوباره امتحان کنید.", + "Abort": "لغوکردن", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "آیا مطمئن هستید که می‌خواهید ایجاد هاست را لغو کنید؟ این فرآیند را نمی‌توان ادامه داد.", + "Confirm abort of host creation": "لغو ایجاد هاست را تأیید کنید", + "Please view existing bugs on Github first. No match? Start a new one.": "لطفاً ابتدا اشکالات موجود را در گیتهاب برنامه را مشاهده کنید. با اشکال شما مطابقتی وجود ندارد؟ مورد جدیدی را ثبت کنید.", + "Report a bug": "گزارش اشکال", + "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "نکته‌ای برای کاربران حرفه‌ای: اگر به مشکل نرم‌افزاری در برنامه برخورد کردید، لطفاً لاگ‌های مشکل را ارسال کنید تا به ما در ردیابی و رفع آن کمک کند.", + "There are two ways you can provide feedback and help us improve %(brand)s.": "به دو روش می توانید بازخوردهای خود را برای کمک به ما در بهبود %(brand)s برسانید.", + "Comment": "نظر", + "Add comment": "افزودن نظر", + "Please go into as much detail as you like, so we can track down the problem.": "لطفاً به اندازه دلخواه با جزئیات توضیح دهید تا بتوانیم مشکل را ردیابی و حل کنیم.", + "Tell us below how you feel about %(brand)s so far.": "در زیر به ما بگویید که تاکنون چه احساسی نسبت به %(brand)s داشته‌اید.", + "Rate %(brand)s": "به %(brand)s امتیاز دهید", + "Feedback sent": "بازخورد ارسال شد", + "Update community": "به‌روزرسانی اجتماع", + "There was an error updating your community. The server is unable to process your request.": "در به‌روزرسانی اجتماع شما خطایی روی داد. سرور قادر به پردازش درخواست شما نیست.", + "Developer Tools": "ابزارهای توسعه‌دهنده", + "Toolbox": "جعبه ابزار", + "Edit Values": "ویرایش مقادیر", + "Values at explicit levels in this room:": "مقادیر در سطوح مشخص در این اتاق:", + "Values at explicit levels:": "مقدار در سطوح مشخص:", + "Value in this room:": "مقدار در این اتاق:", + "Value:": "مقدار:", + "Save setting values": "ذخیره مقادیر تنظیمات", + "Values at explicit levels in this room": "مقادیر در سطوح مشخص در این اتاق", + "Values at explicit levels": "مقادیر در سطوح مشخص", + "Settable at room": "قابل تنظیم در اتاق", + "Settable at global": "قابل تنظیم به شکل سراسری", + "Level": "سطح", + "Setting definition:": "تعریف تنظیم:", + "This UI does NOT check the types of the values. Use at your own risk.": "این واسط کاربری تایپ مقادیر را بررسی نمی‌کند. با مسئولیت خود استفاده کنید.", + "Caution:": "احتیاط:", + "Setting:": "تنظیم:", + "Value in this room": "مقدار در این اتاق", + "Value": "مقدار", + "Setting ID": "شناسه تنظیم", + "Failed to save settings": "تنظیمات ذخیره نشد", + "Settings Explorer": "تنظیمات جستجوگر", + "There was an error finding this widget.": "هنگام یافتن این ابزارک خطایی روی داد.", + "Active Widgets": "ابزارک‌های فعال", + "Search names and descriptions": "جستجوی نام‌ها و توضیحات", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "اگر نمی‌توانید اتاقی را که به دنبال آن می‌گردید پیدا کنید ، بخواهید که شما را به آن دعوت کنند یا یک اتاق جدید بسازید.", + "To view %(spaceName)s, turn on the Spaces beta": "برای مشاهده %(spaceName)s، قابلیت فضای کاری بتا را فعال نمائید", + "To join %(spaceName)s, turn on the Spaces beta": "برای پیوستن به %(spaceName)s، قابلیت فضای کاری بتا را فعال نمائید", + "Failed to create initial space rooms": "ایجاد اتاق‌های اولیه در فضای کاری موفق نبود", + "What do you want to organise?": "چه چیزی را می‌خواهید سازماندهی کنید؟", + "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "اگر اتاق فقط برای همکاری با تیم های داخلی در سرور خانه شما استفاده شود ، ممکن است این قابلیت را فعال کنید. این بعدا نمی تواند تغییر کند.", + "Enable end-to-end encryption": "فعال کردن رمزنگاری سرتاسر", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "گفتگوهای خصوصی یا اتاق‌هایی را برای افزودن انتخاب کنید. این فقط یک فضای کاری برای شماست، هیچ کس از وجود آن مطلع نخواهد شد. می‌توانید موارد بیشتری را بعدا اضافه کنید.", + "Your server requires encryption to be enabled in private rooms.": "سرور شما به گونه‌ای تنظیم شده‌است که فعال بودن رمزنگاری سرتاسر در اتاق‌های خصوصی اجباری می‌باشد.", + "You can’t disable this later. Bridges & most bots won’t work yet.": "بعداً نمی توانید این را لغو کنید. پل‌ها و بیشتر ربات‌ها هنوز کار نمی کنند.", + "Share %(name)s": "به اشتراک‌گذاری %(name)s", + "It's just you at the moment, it will be even better with others.": "در حال حاضر فقط شما حضور دارید ، با دیگران حتی بهتر هم خواهد بود.", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "اتاق‌های خصوصی را فقط با دعوت می‌توان یافت و به آن‌ها پیوست. اتاق‌های عمومی را هر کسی در این اجتماع می‌تواند بیابد و به آن بپیوندد.", + "Go to my first room": "برو به اتاق اول من", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "اتاق‌های خصوصی را فقط با دعوت می‌توان یافت و به آن‌ها پیوست. اما اتاق‌های عمومی را هر کسی می تواند را پیدا کند و به آن‌ها ملحق شود.", + "Please enter a name for the room": "لطفاً نامی برای اتاق وارد کنید", + "Community ID": "شناسه اجتماع", + "Community Name": "نام اجتماع", + "Create Community": "ایجاد اجتماع", + "Something went wrong whilst creating your community": "هنگام ایجاد اجتماع مشکلی پیش آمد", + "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "شناسه اجتماع باید فقط حاوی کاراکتر‌های a-z، 0-9 یا '=_-./' باشد", + "Community IDs cannot be empty.": "شناسه اجتماع نمی تواند خالی باشد.", + "An image will help people identify your community.": "تصویر به افراد کمک می کند تا با سهولت بیشتری اجتماع شما پیدا کنند.", + "Add image (optional)": "افزودن تصویر (اختیاری)", + "Enter name": "ورود نام", + "What's the name of your community or team?": "نام اجتماع یا تیم شما چیست؟", + "You can change this later if needed.": "در صورت نیاز می توانید بعداً این مورد را تغییر دهید.", + "Use this when referencing your community to others. The community ID cannot be changed.": "هنگام ارجاع دادن دیگران به اجتماع خود، از این مورد استفاده کنید. شناسه اجتماع قابل تغییر نیست.", + "Community ID: +:%(domain)s": "شناسه اجتماع: %(domain)s:+", + "There was an error creating your community. The name may be taken or the server is unable to process your request.": "هنگام ایجاد اجتماع خطایی روی داد. ممکن است نام گرفته شده‌باشد و یا اینکه سرور نتواند درخواست شما را پردازش کند.", + "Clear all data": "پاک کردن همه داده ها", + "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "پاک کردن همه داده های این جلسه غیرقابل بازگشت است. پیامهای رمزگذاری شده از بین می‌روند مگر اینکه از کلیدهای آنها پشتیبان تهیه شده باشد.", + "Clear all data in this session?": "همه داده‌های این نشست پاک شود؟", + "Go to my space": "برو به محیط کاری من", + "Who are you working with?": "با چه کسانی کار می‌کنید؟", + "Make sure the right people have access to %(name)s": "اطمینان حاصل کنید که افراد مناسب به %(name)s دسترسی دارند", + "Just me": "فقط من", + "A private space to organise your rooms": "یک فضای کار خصوصی برای منظم‌کردن اتاق‌هایتان", + "Me and my teammates": "من و هم‌تیمی‌هایم", + "A private space for you and your teammates": "یک فضای کار خصوصی برای شما و هم تیمی‌هایتان", + "Reason (optional)": "دلیل (اختیاری)", + "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "آیا مطمئن هستید که می خواهید این رویداد را حذف کنید؟ توجه داشته باشید که اگر نام اتاق یا تغییر موضوع را حذف کنید، این تغییر می تواند لغو شود.", + "Failed to invite the following users to your space: %(csvUsers)s": "امکان دعوت کاربرانی که در ادامه آمده‌اند به فضای کاری شما میسر نیست: %(csvUsers)s", + "Invite your teammates": "هم‌تیمی‌های خود را دعوت کنید", + "Make sure the right people have access. You can invite more later.": "اطمینان حاصل کنید که افراد مناسب دسترسی دارند. بعداً می توانید افراد بیشتری دعوت کنید.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "این یک قابلیت آزمایشی است. برای الان، کاربران جدیدی که دعوتنامه دریافت می‌کنند باید آن را بر روی باز کنند تا بتوانند عضو شوند.", + "Invite by username": "دعوت به نام کاربری", + "What are some things you want to discuss in %(spaceName)s?": "برخی از مواردی که می خواهید درباره‌ی آن‌ها در %(spaceName)s بحث کنید، چیست؟", + "Let's create a room for each of them.": "بیایید برای هر یک از آنها یک اتاق درست کنیم.", + "You can add more later too, including already existing ones.": "بعداً می توانید موارد بیشتری را اضافه کنید ، از جمله موارد موجود.", + "What projects are you working on?": "روی چه پروژه‌هایی کار می‌کنید؟", + "Confirm Removal": "تأیید حذف", + "Removing…": "در حال حذف…", + "Invite people to join %(communityName)s": "از افراد برای پیوستن به %(communityName)s دعوت کنید", + "Send %(count)s invites|one": "ارسال %(count)s دعوت", + "Send %(count)s invites|other": "ارسال %(count)s دعوت", + "Show": "نمایش", + "Hide": "پنهان کردن", + "People you know on %(brand)s": "افرادی که در %(brand)s می‌شناسید", + "Add another email": "ایمیل دیگری اضافه کنید", + "Unable to load commit detail: %(msg)s": "بارگیری جزئیات commit انجام نشد: %(msg)s", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "اگر زمینه دیگری وجود دارد که می تواند به تجزیه و تحلیل مسئله کمک کند، مانند آنچه در آن زمان انجام می دادید، شناسه اتاق، شناسه کاربر و غیره ، لطفاً موارد ذکر شده را در اینجا وارد کنید.", + "Notes": "یادداشت‌ها", + "We'll create rooms for each of them. You can add more later too, including already existing ones.": "ما برای هر یک از آنها اتاق ایجاد خواهیم کرد. بعداً می توانید موارد دیگر را اضافه کنید ، از جمله موارد موجود.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "سعی شد یک نقطه‌ی زمانی خاص در پیام‌های این اتاق بارگیری و نمایش داده شود، اما شما دسترسی لازم برای مشاهده‌ی پیام را ندارید.", + "GitHub issue": "مسئله GitHub", + "Download logs": "دانلود گزارش‌ها", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "قبل از ارسال گزارش‌ها، برای توصیف مشکل خود باید یک مسئله در GitHub ایجاد کنید.", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "سعی شد یک نقطه‌ی زمانی خاص در پیام‌های این اتاق بارگیری و نمایش داده شود، اما پیداکردن آن میسر نیست.", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "گزارش‌های اشکال زدایی حاوی داده‌های استفاده از برنامه شامل نام کاربری شما، شناسه‌ها یا نام مستعار اتاق‌ها یا گروه‌هایی است که بازدید کرده‌اید و همچنین نام‌ کاربری کاربران دیگر. این گزارش‌ها حاوی پیام‌های شما نمی‌باشند.", + "Failed to load timeline position": "بارگیری و نمایش پیام‌ها با مشکل مواجه شد", + "Uploading %(filename)s and %(count)s others|other": "در حال بارگذاری %(filename)s و %(count)s مورد دیگر", + "Uploading %(filename)s and %(count)s others|zero": "در حال بارگذاری %(filename)s", + "Uploading %(filename)s and %(count)s others|one": "در حال بارگذاری %(filename)s و %(count)s مورد دیگر", + "Failed to find the general chat for this community": "گفتگوی عمومی برای این اجتماع پیدا نشد", + "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "یادآوری: مرورگر شما پشتیبانی نمی شود ، بنابراین ممکن است تجربه شما غیرقابل پیش بینی باشد.", + "Preparing to download logs": "در حال آماده سازی برای بارگیری گزارش ها", + "Got an account? Sign in": "حساب کاربری دارید؟ وارد شوید", + "Failed to send logs: ": "ارسال گزارش با خطا مواجه شد: ", + "New here? Create an account": "تازه وارد هستید؟ یک حساب کاربری ایجاد کنید", + "Thank you!": "با سپاس!", + "The email address linked to your account must be entered.": "آدرس ایمیلی که به حساب کاربری شما متصل است، باید وارد شود.", + "Logs sent": "گزارش‌های مربوط ارسال شد", + "Preparing to send logs": "در حال آماده سازی برای ارسال گزارش ها", + "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "لطفاً به ما بگویید چه مشکلی پیش آمد و یا اینکه لطف کنید و یک مسئله GitHub ایجاد کنید که مشکل را توصیف کند.", + "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "تغییر گذرواژه ، تمام کلیدهای رمزنگاریِ سرتاسر در تمام نشست‌های شما را پاک کرده و تاریخچه چت‌های رمزشده را غیرقابل خواندن می‌کند. قبل از تغییر گذرواژه خود ، پشتیبان‌گیری از کلید‌ها را تنظیم یا کلیدهای اتاق‌های خود را از نشست دیگری استخراج (Export) کنید.", + "Send feedback": "ارسال بازخورد", + "You may contact me if you have any follow up questions": "در صورت داشتن هرگونه سوال پیگیری ممکن است با من تماس بگیرید", + "Feedback": "بازخورد", + "To leave the beta, visit your settings.": "برای خروج از بتا به بخش تنظیمات مراجعه کنید.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "سیستم‌عامل و نام کاربری شما ثبت خواهد شد تا به ما کمک کند تا جایی که می توانیم از نظرات شما استفاده کنیم.", + "%(featureName)s beta feedback": "بازخورد بتا برای %(featureName)s", + "A verification email will be sent to your inbox to confirm setting your new password.": "برای تأیید تنظیم گذرواژه جدید ، یک ایمیل تأیید به صندوق ورودی شما ارسال می شود.", + "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "یک ایمیل به %(emailAddress)s ارسال شده‌است. بعد از کلیک بر روی لینک داخل آن ایمیل، روی مورد زیر کلیک کنید.", + "Done": "انجام شد", + "Thank you for your feedback, we really appreciate it.": "از بازخورد شما متشکریم ، ما واقعاً از آن استقبال می کنیم.", + "Beta feedback": "بازخورد بتا", + "Close dialog": "بستن گفتگو", + "Invite anyway": "به هر حال دعوت کن", + "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "شما از همه نشست‌ها خارج شده و دیگر اعلان پیام‌ها را دریافت نخواهید کرد. برای فعال کردن مجدد اعلان‌ها در هر دستگاه، دوباره وارد شوید.", + "Invite anyway and never warn me again": "به هر حال دعوت کن و دیگر هرگز به من هشدار نده", + "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "برای شناسه‌های ماتریکس زیر، پروفایلی پیدا نشد - آیا به هر حال می خواهید آنها را دعوت کنید؟", + "Invalid homeserver discovery response": "پاسخ جستجوی سرور معتبر نیست", + "Failed to get autodiscovery configuration from server": "دریافت پیکربندیِ جستجوی خودکار از سرور موفقیت‌آمیز نبود", + "The following users may not exist": "کاربران زیر ممکن است وجود نداشته باشند", + "Use an identity server to invite by email. Manage in Settings.": "از یک سرور هویت‌سنجی برای دعوت از طریق ایمیل استفاده کنید. اینکار را می‌توانید از طریق بخش تنظیمات انجام دهید.", + "Invalid base_url for m.homeserver": "base_url نامعتبر برای m.homeserver", + "Homeserver URL does not appear to be a valid Matrix homeserver": "به نظر می‌رسد که آدرس سرور، متعلق به یک سرور معتبر نباشد", + "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "از یک سرور هویت‌سنجی برای دعوت از طریق ایمیل استفاده کنید. از پیش فرض (%(defaultIdentityServerName)s) استفاده کنید یا آن را در بخش تنظیمات مدیریت کنید.", + "Invalid identity server discovery response": "پاسخ نامعتبر برای جستجوی سرور هویت‌سنجی", + "Invalid base_url for m.identity_server": "base_url نامعتبر برای سرور m.identity_server", + "Identity server URL does not appear to be a valid identity server": "به نظر می‌رسد آدرس سرور هویت‌سنجی، متعلق به یک سرور هویت‌سنجی معتبر نیست", + "Try using one of the following valid address types: %(validTypesList)s.": "از یکی از انواع آدرس‌های معتبر زیر استفاده کنید: %(validTypesList)s.", + "Wrong file type": "نوع فایل اشتباه است", + "Confirm encryption setup": "راه‌اندازی رمزگذاری را تأیید کنید", + "You have entered an invalid address.": "آدرسی که وارد کرده‌اید نامعتبر است.", + "General failure": "خطای عمومی", + "That doesn't look like a valid email address": "آدرس ایمیل نامعتبر است", + "This homeserver does not support login using email address.": "این سرور از ورود با استفاده از آدرس ایمیل پشتیبانی نمی کند.", + "Please contact your service administrator to continue using this service.": "لطفاً برای ادامه استفاده از این سرویس با مدیر سرور خود تماس بگیرید .", + "email address": "آدرس ایمیل", + "Matrix Room ID": "شناسه اتاق ماتریکس", + "Matrix ID": "شناسه ماتریکس", + "Create a new room": "ایجاد اتاق جدید", + "This account has been deactivated.": "این حساب غیر فعال شده است.", + "Please note you are logging into the %(hs)s server, not matrix.org.": "لطفا توجه کنید شما به سرور %(hs)s وارد شده‌اید، و نه سرور matrix.org.", + "Want to add a new room instead?": "آیا می‌خواهید یک اتاق جدید را بیفزایید؟", + "Add existing rooms": "افزودن اتاق‌های موجود", + "Space selection": "انتخاب فضای کاری", + "There was a problem communicating with the homeserver, please try again later.": "در برقراری ارتباط با سرور مشکلی پیش آمده، لطفاً چند لحظه‌ی دیگر مجددا امتحان کنید.", + "Filter your rooms and spaces": "اتاق‌ها و فضاهای کاری خود را فیلتر کنید", + "Adding rooms... (%(progress)s out of %(count)s)|one": "در حال افزودن اتاق‌ها...", + "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "امکان اتصال به سرور از طریق پروتکل‌های HTTP و HTTPS در مروگر شما میسر نیست. یا از HTTPS استفاده کرده و یا حالت اجرای غیرامن اسکریپت‌ها را فعال کنید.", + "Adding rooms... (%(progress)s out of %(count)s)|other": "در حال افزودن اتاق‌ها... (%(progress)s از %(count)s)", + "Not all selected were added": "همه‌ی موارد انتخاب شده، اضافه نشدند", + "Matrix rooms": "اتاق‌های ماتریکس", + "%(networkName)s rooms": "اتاق‌های %(networkName)s", + "Add a new server...": "افزودن سرور جدید ...", + "Server name": "نام سرور", + "Enter the name of a new server you want to explore.": "نام سرور جدیدی که می خواهید در آن کاوش کنید را وارد کنید.", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "اتصال به سرور میسر نیست - لطفا اتصال اینترنت خود را بررسی کنید؛ اطمینان حاصل کنید گواهینامه‌ی SSL سرور شما قابل اعتماد است، و اینکه پلاگینی بر روی مرورگر شما مانع از ارسال درخواست به سرور نمی‌شود.", + "Add a new server": "افزودن سرور جدید", + "Matrix": "ماتریکس", + "Remove server": "حذف سرور", + "Are you sure you want to remove %(serverName)s": "آیا مطمئن هستید که می خواهید %(serverName)s را حذف کنید", + "Your server": "سرور شما", + "Can't find this server or its room list": "این سرور و یا لیست اتاق‌های آن پیدا نمی شود", + "You are not allowed to view this server's rooms list": "شما مجاز به مشاهده لیست اتاق‌های این سرور نمی‌باشید", + "Looks good": "به نظر خوب میاد", + "Unable to query for supported registration methods.": "درخواست از روش‌های پشتیبانی‌شده‌ی ثبت‌نام میسر نیست.", + "Enter a server name": "نام سرور را وارد کنید", + "And %(count)s more...|other": "و %(count)s مورد بیشتر ...", + "Sign in with single sign-on": "با احراز هویت یکپارچه وارد شوید", + "This server does not support authentication with a phone number.": "این سرور از قابلیت احراز با شماره تلفن پشتیبانی نمی کند.", + "Continue with %(provider)s": "با %(provider)s ادامه دهید", + "That username already exists, please try another.": "این نام کاربری از قبل وجود دارد ، لطفاً نام دیگری را امتحان کنید.", + "Homeserver": "سرور", + "Continue with %(ssoButtons)s": "با %(ssoButtons)s ادامه بده", + "Join millions for free on the largest public server": "به بزرگترین سرور عمومی با میلیون ها نفر کاربر بپیوندید", + "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s یا %(usernamePassword)s", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "حساب جدید شما (%(newAccountId)s) s) ثبت شده‌است ، اما شما قبلاً به حساب کاربری دیگری (%(loggedInUserId)s) وارد شده‌اید.", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "می توانید از طریق گزینه‌ی سرور سفارشی با استفاده از آدرس سروری دیگر، به دیگر سرورهای Matrix متصل شوید. با این کار می توانید از Element جهت اتصال به یک اکانت ماتریکس بر روی سروری دیگر استفاده کنید.", + "Continue with previous account": "با حساب کاربری قبلی ادامه دهید", + "Log in to your new account.": "به حساب کاربری جدید خود وارد شوید.", + "You can now close this window or log in to your new account.": "اکنون می توانید این پنجره را ببندید یا به حساب کاربری جدید خود وارد شوید.", + "Use another login": "از ورود دیگری استفاده کنید", + "Failed to re-authenticate due to a homeserver problem": "به دلیل مشکلی که در سرور وجود دارد ، احراز هویت مجدد انجام نشد", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "نمی توانید وارد حساب کاربری خود شوید. لطفا برای اطلاعات بیشتر با مدیر سرور خود تماس بگیرید.", + "Server Options": "گزینه های سرور", + "This address is already in use": "این آدرس قبلاً استفاده شده‌است", + "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.": "هشدار: داده های شخصی شما (از جمله کلیدهای رمزنگاری) هنوز در این نشست ذخیره می شوند. اگر استفاده از این نشست را به پایان رسانده‌اید یا می‌خواهید وارد حساب کاربری دیگری شوید ، آن را پاک کنید.", + "This address is available to use": "این آدرس برای استفاده در دسترس است", + "Please provide a room address": "لطفاً آدرس اتاق را ارائه تعیین کنید", + "e.g. my-room": "به عنوان مثال، my-room", + "Some characters not allowed": "برخی از کاراکترها مجاز نیستند", + "Command Autocomplete": "تکمیل خودکار دستور", + "Community Autocomplete": "تکمیل خودکار اجتماع", + "Room address": "آدرس اتاق", + "Results from DuckDuckGo": "نتایج از موتور جستجوی DuckDuckGo", + "In reply to ": "در پاسخ به", + "DuckDuckGo Results": "نتایج موتور جستجوی DuckDuckGo", + "Emoji Autocomplete": "تکمیل خودکار شکلک", + "Notify the whole room": "به کل اتاق اطلاع بده", + "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "بارگیری رویدادی که به آن پاسخ داده شد امکان پذیر نیست، یا وجود ندارد یا شما اجازه مشاهده آن را ندارید.", + "Room Notification": "اعلان اتاق", + "Notification Autocomplete": "تکمیل خودکار اعلان", + "Room Autocomplete": "تکمیل خودکار اتاق", + "Space Autocomplete": "تکمیل خودکار فضای کاری", + "User Autocomplete": "تکمیل خودکار کاربر", + "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "ما یک نسخه رمزگذاری شده از کلیدهای شما را در سرور خود ذخیره خواهیم کرد. پشتیبان خود را با یک عبارت امنیتی ایمن کنید.", + "For maximum security, this should be different from your account password.": "برای حداکثر امنیت ، این باید با گذرواژه‌ی حساب شما متفاوت باشد.", + "Enter a Security Phrase": "یک عبارت امنیتی وارد کنید", + "Great! This Security Phrase looks strong enough.": "عالی! این عبارت امنیتی به اندازه کافی قوی به نظر می رسد.", + "QR Code": "کد QR", + "Custom level": "سطح دلخواه", + "Set up with a Security Key": "یک کلید امنیتی تنظیم کنید", + "Power level": "سطح قدرت", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s هیچ تغییری ایجاد نکرد", + "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s %(count)s مرتبه هیچ تغییری ایجاد نکرد", + "That matches!": "مطابقت دارد!", + "Use a different passphrase?": "از عبارت امنیتی دیگری استفاده شود؟", + "That doesn't match.": "مطابقت ندارد.", + "Go back to set it again.": "برای تنظیم مجدد آن به عقب برگردید.", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s هیچ تغییری ایجاد نکردند", + "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s %(count)s تغییری ایجاد نکردند", + "Enter your Security Phrase a second time to confirm it.": "عبارت امنیتی خود را برای تائید مجددا وارد کنید.", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s آواتار خود را تغییر داد", + "Repeat your Security Phrase...": "عبارت امنیتی خود را تکرار کنید ...", + "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "کلید امنیتی شما یک راهکار امنیتی است - اگر عبارت امنیتی خود را فراموش کنید ، می توانید از آن برای بازیابی دسترسی به پیام‌های رمز‌شده خود استفاده کنید.", + "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s آواتار خود را %(count)s مرتبه تغییر داده‌است", + "Keep a copy of it somewhere secure, like a password manager or even a safe.": "یک نسخه از آن را در جایی امن نگهداری کنید.", + "Your Security Key": "کلید امنیتی شما", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s آواتار خود را تغییر دادند", + "Your Security Key has been copied to your clipboard, paste it to:": "از کلید امنیتی شما رونوشت گرفته شده است، آن را الصاق کنید به:", + "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s %(count)s مرتبه آواتار خود را تغییر دادند", + "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s نام خود را تغییر داد", + "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s نام خود را %(count)s مرتبه تغییر داد", + "Your Security Key is in your Downloads folder.": "کلید امنیتی شما در پوشه‌ Downloads قرار دارد.", + "Print it and store it somewhere safe": "این را پرینت کرده و در جایی امن نگهداری کنید", + "Save it on a USB key or backup drive": "بر روی فلش مموری یا پارتیشن پشتیبان ذخیره کنید", + "Copy it to your personal cloud storage": "روی فضای شخصی خودتان نسخه‌ی رونوشت بگیرید", + "Your keys are being backed up (the first backup could take a few minutes).": "در حال پیشتیبان‌گیری از کلیدهای شما (اولین نسخه پشتیبان ممکن است چند دقیقه طول بکشد).", + "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s نام خود را تغییر دادند", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "بدون تنظیم امکان پشتیبان‌گیریِ امن پیام‌ها، در صورت ورود به سیستم یا استفاده از نشست دیگر ، نمی توانید تاریخچه‌ی پیام‌های رمزشده خود را بازیابی کنید.", + "Set up Secure Message Recovery": "امکان بازیابی پیام‌های امن را تنظیم کنید", + "Secure your backup with a Security Phrase": "نسخه‌ی پشتیبان خود را با استفاده از عبارت امنیتی امن کنید", + "Confirm your Security Phrase": "عبارت امنیتی خود را تأیید کنیدعبارت امنیتی خود را تائید نمائید", + "Make a copy of your Security Key": "از کلید امنیتی خود رونوشت بگیرید", + "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s نام خود را %(count)s بار تغییر دادند", + "was kicked %(count)s times|one": "اخراج شد", + "Starting backup...": "آغاز پشتیبان‌گیری...", + "Success!": "موفقیت‌آمیز بود!", + "Create key backup": "ساختن نسخه‌ی پشتیبان کلید", + "Unable to create key backup": "ایجاد کلید پشتیبان‌گیری امکان‌پذیر نیست", + "Generate a Security Key": "یک کلید امنیتی ایجاد کنید", + "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "ما یک کلید امنیتی برای شما تولید کردیم تا آن را در یک جای امن ذخیره کنید.", + "was kicked %(count)s times|other": "%(count)s بار اخراج شد", + "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "از یک عبارت محرمانه که فقط خودتان می‌دانید استفاده کنید، و محض احتیاط کلید امینی خود را برای استفاده هنگام پشتیبان‌گیری ذخیره نمائید.", + "were kicked %(count)s times|one": "اخراج شدند", + "were kicked %(count)s times|other": "%(count)s بار اخراج شدند", + "was unbanned %(count)s times|one": "رفع تحریم شد", + "was unbanned %(count)s times|other": "%(count)s بار رفع تحریم شد", + "were unbanned %(count)s times|one": "رفع تحریم شد", + "were unbanned %(count)s times|other": "%(count)s بار رفع تحریم شد", + "Verification Requests": "درخواست های تأیید", + "View Servers in Room": "مشاهده سرورها در اتاق", + "Explore Account Data": "کاوش داده‌های حساب کاربری", + "Explore Room State": "کاوش حالت اتاق", + "Filter results": "پالایش نتایج", + "Send Account Data": "ارسال اطلاعات حساب کاربری", + "Event Content": "محتوای رخداد", + "State Key": "کلید حالت", + "Event Type": "نوع رخداد", + "Failed to send custom event.": "رخداد سفارشی ارسال نشد.", + "Event sent!": "رخداد ارسال شد!", + "You must specify an event type!": "شما باید نوع رخداد را مشخص کنید!", + "Send Custom Event": "ارسال رخداد سفارشی", + "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "لطفا بعد از غیرفعال کردن حساب کاربری، تمام پیام‌هایی را که ارسال کرده‌ام، فراموش کن ( هشدار: این امر باعث می شود کاربران آینده نمای ناقصی از مکالماتی که شما در آن‌ها حضور داشته‌اید را مشاهده کنند)", + "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "قابلیت مشاهده پیام در پروتکل ماتریکس مانند ایمیل است. فراموش کردن پیام‌های شما به این معنی است که پیام‌هایی که ارسال کرده‌اید با هیچ کاربر جدید یا ثبت نشده‌ای به اشتراک گذاشته نخواهد شد ، اما کاربران ثبت نام شده‌ای که از قبل به این پیام ها دسترسی دارند همچنان دسترسی خود را خواهند داشت.", + "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "غیرفعال کردن حساب شما به طور پیش فرض باعث نمی شود پیام‌های ارسالی شما را فراموش کنیم. اگر می‌خواهید پیام های شما را فراموش کنیم ، لطفاً کادر زیر را علامت بزنید.", + "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "این کار باعث می شود حساب کاربری شما برای همیشه غیرقابل استفاده شود. شما قادر به ورود به برنامه نخواهید بود و هیچ کس نمی تواند همان شناسه کاربری مشابه را دوباره ثبت کند. این باعث می شود که حساب شما از همه اتاق هایی که در آن‌ها عضو بودید خارج شده و جزئیات حساب شما از سرور هویت‌سنجی حذف گردد. این عمل برگشت‌ناپذیر است.", + "Server did not return valid authentication information.": "سرور اطلاعات احراز هویت معتبری را باز نگرداند.", + "Server did not require any authentication": "سرور به احراز هویت احتیاج نداشت", + "There was a problem communicating with the server. Please try again.": "مشکلی در برقراری ارتباط با سرور وجود داشت. لطفا دوباره تلاش کنید.", + "To continue, please enter your password:": "برای ادامه لطفا گذرواژه خود را وارد کنید:", + "Confirm account deactivation": "غیرفعال کردن حساب کاربری را تأیید کنید", + "Are you sure you want to deactivate your account? This is irreversible.": "آیا از غیرفعال‌کردن حساب کاربری خود اطمینان دارید؟ این کار غیر قابل بازگشت است.", + "Confirm your account deactivation by using Single Sign On to prove your identity.": "برای غیرفعال‌کردن حساب کاربری خود ابتدا باید هویت خود را ثابت کنید که برای این کار می‌توانید از احراز هویت یکپارچه استفاده کنید.", + "Continue With Encryption Disabled": "با رمزنگاری غیرفعال ادامه بده", + "Incompatible Database": "پایگاه داده ناسازگار", + "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "شما قبلاً با این نشست از نسخه جدیدتر %(brand)s استفاده کرده‌اید. برای استفاده مجدد از این نسخه با قابلیت رمزنگاری سرتاسر ، باید از حسابتان خارج شده و دوباره وارد برنامه شوید.", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "برای جلوگیری از دست دادن تاریخچه‌ی گفتگوی خود باید قبل از ورود به برنامه ، کلیدهای اتاق خود را استخراج (Export) کنید. برای این کار باید از نسخه جدیدتر %(brand)s استفاده کنید", + "Sign out": "خروج از حساب کاربری", + "Block anyone not part of %(serverName)s from ever joining this room.": "از عضوشدن کاربرانی در این اتاق که حساب آن‌ها متعلق به سرور %(serverName)s است، جلوگیری کن.", + "Make this room public": "این اتاق را عمومی کنید", + "Topic (optional)": "موضوع (اختیاری)", + "Create a room in %(communityName)s": "یک اتاق در %(communityName)s بسازید", + "Create a private room": "ساختن اتاق خصوصی", + "Create a public room": "ساختن اتاق عمومی", + "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "اگر از اتاق برای همکاری با تیم های خارجی که سرور خود را دارند استفاده شود ، ممکن است این را غیرفعال کنید. این نمی‌تواند بعدا تغییر کند.", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "اگر نمی توانید اتاقی را که به دنبال آن می گردید پیدا کنید ، از یکی از اعضای آن بخواهید شما را دعوت کند و یا یک اتاق جدید بسازید.", + "You can't send any messages until you review and agree to our terms and conditions.": "تا زمانی که شرایط و ضوابط سرویس ما را مطالعه و با آن موافقت نکنید، نمی توانید هیچ پیامی ارسال کنید.", + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "پیام شما ارسال نشد زیرا این سرور به محدودیت تعداد کاربر فعال ماهانه‌ی خود رسیده است. لطفاً برای ادامه استفاده از سرویس با مدیر سرور خود تماس بگیرید .", + "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "پیام شما ارسال نشد زیرا این سرور توسط مدیر آن مسدود شده است. لطفاً برای ادامه استفاده از سرویس با مدیر سرور خود تماس بگیرید .", + "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "پیام شما ارسال نشد زیرا این سرور از محدودیت منابع فراتر رفته است. لطفاً برای ادامه استفاده از سرویس با مدیر سرور خود تماس بگیرید .", + "Some of your messages have not been sent": "بعضی از پیام‌های شما ارسال نشده‌اند", + "You can select all or individual messages to retry or delete": "شما می‌توانید یک یا همه‌ی پیام‌ها را برای تلاش مجدد یا حذف انتخاب کنید", + "Sent messages will be stored until your connection has returned.": "پیام‌های ارسالی تا زمان بازگشت اتصال شما ذخیره خواهند ماند.", + "Server may be unavailable, overloaded, or search timed out :(": "سرور ممکن است در دسترس نباشد ، بار زیادی روی آن قرار گرفته یا زمان جستجو به پایان رسیده‌باشد :(", + "You have %(count)s unread notifications in a prior version of this room.|other": "شما %(count)s اعلان خوانده‌نشده در نسخه‌ی قبلی این اتاق دارید.", + "You have %(count)s unread notifications in a prior version of this room.|one": "شما %(count)s اعلان خوانده‌نشده در نسخه‌ی قبلی این اتاق دارید.", + "%(count)s members|other": "%(count)s عضو", + "%(count)s members|one": "%(count)s عضو", + "%(count)s rooms|other": "%(count)s اتاق", + "%(count)s rooms|one": "%(count)s اتاق", + "This room is suggested as a good one to join": "این اتاق به عنوان یک گزینه‌ی خوب برای عضویت پیشنهاد می شود", + "Suggested": "پیشنهادی", + "Your server does not support showing space hierarchies.": "سرور شما از نمایش سلسله مراتبی فضاهای کاری پشتیبانی نمی کند.", + "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s اتاق و %(numSpaces)s فضای کاری", + "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s اتاق و %(numSpaces)s فضای کاری", + "%(count)s rooms and 1 space|other": "%(count)s اتاق و یک فضای کاری", + "%(count)s rooms and 1 space|one": "%(count)s اتاق و یک فضای‌کاری", + "Select a room below first": "ابتدا یک اتاق از لیست زیر انتخاب کنید", + "Failed to remove some rooms. Try again later": "حذف برخی اتاق‌ها با مشکل همراه بود. لطفا بعدا تلاش فرمائید", + "Mark as not suggested": "علامت‌گذاری به عنوان پیشنهاد‌نشده", + "Mark as suggested": "علامت‌گذاری به عنوان پیشنهاد‌شده", + "You may want to try a different search or check for typos.": "ممکن است بخواهید یک جستجوی دیگر انجام دهید یا غلط‌های املایی را بررسی کنید.", + "was banned %(count)s times|one": "تحریم شد", + "was banned %(count)s times|other": "%(count)s بار تحریم شد", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "برای در امان ماندن در برابر از دست‌دادن پیام‌ها و داده‌های رمزشده‌ی خود، از کلید‌های رمزنگاری خود یک نسخه‌ی پشتیبان بر روی سرور قرار دهید.", + "were banned %(count)s times|one": "تحریم شد", + "were banned %(count)s times|other": "%(count)s بار تحریم شد", + "was invited %(count)s times|one": "دعوت شد", + "was invited %(count)s times|other": "%(count)s بار دعوت شده است", + "Enter your account password to confirm the upgrade:": "گذرواژه‌ی خود را جهت تائيد عملیات ارتقاء وارد کنید:", + "were invited %(count)s times|one": "دعوت شدند", + "were invited %(count)s times|other": "%(count)s بار دعوت شده‌اند", + "Restore your key backup to upgrade your encryption": "برای ارتقاء رمزنگاری، ابتدا نسخه‌ی پشتیبان خود را بازیابی کنید", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)s دعوت خود را پس گرفته است", + "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)s دعوت خود را %(count)s مرتبه پس‌گرفته‌است", + "Restore": "بازیابی", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)s دعوت‌های خود را پس‌گرفتند", + "%(name)s cancelled verifying": "%(name)s تأیید هویت را لغو کرد", + "You cancelled verifying %(name)s": "شما تأیید هویت %(name)s را لغو کردید", + "You verified %(name)s": "شما هویت %(name)s را تأیید کردید", + "You have ignored this user, so their message is hidden. Show anyways.": "شما این کاربر را نادیده گرفته‌اید، بنابراین پیام او نمایش داده نمی‌شود. نمایش بده.", + "Video conference started by %(senderName)s": "کنفرانس ویدئویی توسط %(senderName)s آغاز شده است", + "Video conference updated by %(senderName)s": "کنفرانس ویدیویی توسط %(senderName)s به روز شد", + "Video conference ended by %(senderName)s": "کنفرانس ویدیویی توسط %(senderName)s به پایان رسید", + "Join the conference from the room information card on the right": "از طریق کارت اطلاعات اتاق در سمت راست، به کنفرانس بپیوندید", + "Join the conference at the top of this room": "از بالای این اتاق به کنفرانس بپوندید", + "Show image": "نمایش تصویر", + "Error decrypting image": "خطا در رمزگشایی تصویر", + "Invalid file%(extra)s": "پرونده نامعتبر%(extra)s", + "Message Actions": "اقدامات پیام", + "Reply": "پاسخ", + "Retry": "تلاش مجدد", + "Edit": "ویرایش", + "React": "واکنش", + "Error decrypting audio": "خطا در رمزگشایی صدا", + "The encryption used by this room isn't supported.": "رمزگذاری استفاده شده توسط این اتاق پشتیبانی نمی شود.", + "Encryption not enabled": "رمزگذاری فعال نیست", + "Ignored attempt to disable encryption": "تلاش برای غیرفعال کردن رمزگذاری نادیده گرفته شد", + "Encryption enabled": "رمزگذاری فعال است", + "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "پیام های موجود در این اتاق به صورت سرتاسر رمزگذاری شده‌اند. هنگام ورود افراد، می توانید آن‌ها را از طریق نمایه آن‌ها تأیید کنید، کافی است روی آواتار آن‌ها ضربه بزنید.", + "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "پیام های اینجا به صورت سرتاسر رمزگذاری شده هستند. %(displayName)s را در نمایه خود تأیید کنید - روی آواتار او ضربه بزنید.", + "Compare emoji": "مقایسه شکلک", + "Verification cancelled": "تأیید هویت لغو شد", + "You cancelled verification.": "شما تأیید هویت را لغو کردید.", + "%(displayName)s cancelled verification.": "%(displayName)s تایید هویت را لغو کرد.", + "You cancelled verification on your other session.": "شما تأیید صحت جلسه دیگر خود را لغو کردید.", + "Verification timed out.": "مهلت تأیید تمام شد.", + "Start verification again from their profile.": "دوباره تأیید را از نمایه آنها شروع کنید.", + "Start verification again from the notification.": "از اعلان دوباره تأیید را شروع کنید.", + "Got it": "فهمیدم", + "Verified": "تأیید شد", + "You've successfully verified %(displayName)s!": "شما%(displayName)s را با موفقیت تأیید کردید!", + "You've successfully verified %(deviceName)s (%(deviceId)s)!": "شما با موفقیت %(deviceName)s (%(deviceId)s) را تأیید کردید!", + "You've successfully verified your device!": "شما با موفقیت دستگاه خود را تأیید کردید!", + "In encrypted rooms, verify all users to ensure it’s secure.": "در اتاق های رمزگذاری شده ، برای اطمینان از امنیت اتاق، همه کاربران را تأیید هویت کنید.", + "Verify all users in a room to ensure it's secure.": "برای اطمینان از امنیت اتاق، هویت همه‌ی کاربران حاضر در اتاق را تأیید کنید.", + "Almost there! Is %(displayName)s showing the same shield?": "تقریباً تمام شد! آیا %(displayName)s نیز سپر مشابهی را نشان می‌دهد؟", + "Almost there! Is your other session showing the same shield?": "تقریباً انجام شد! آیا نشست دیگر شما همان سپر را نشان می دهد؟", + "Verify by emoji": "تأیید توسط شکلک", + "Verify by comparing unique emoji.": "با مقایسه شکلک تأیید کنید.", + "If you can't scan the code above, verify by comparing unique emoji.": "اگر نمی توانید کد بالا را اسکن کنید ، با مقایسه شکلک منحصر به فرد، او را تأیید کنید.", + "Ask %(displayName)s to scan your code:": "از %(displayName)s بخواهید که کد شما را اسکن کند:", + "Verify by scanning": "با اسکن تأیید کنید", + "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "نشستی که می خواهید تأیید کنید از اسکن کد QR یا تأیید شکلک پشتیبانی نمی کند ، یعنی همان چیزی که %(brand)s پشتیبانی می کند. با کلاینت دیگری امتحان کنید.", + "Security": "امنیت", + "Edit devices": "ویرایش دستگاه‌ها", + "This client does not support end-to-end encryption.": "این کلاینت از رمزگذاری سرتاسر پشتیبانی نمی کند.", + "Role": "نقش", + "Failed to deactivate user": "غیرفعال کردن کاربر انجام نشد", + "Deactivate user": "غیرفعال کردن کاربر", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "با غیرفعال کردن این کاربر، او از سیستم خارج شده و از ورود مجدد وی جلوگیری می‌شود. علاوه بر این، او تمام اتاق هایی را که در آن هست ترک می کند. این عمل قابل برگشت نیست. آیا مطمئن هستید که می خواهید این کاربر را غیرفعال کنید؟", + "Deactivate user?": "کاربر غیرفعال شود؟", + "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "شما نمی توانید این تغییر را باطل کنید زیرا در حال ارتقا سطح قدرت یک کاربر به سطح قدرت خود هستید.", + "Failed to change power level": "تغییر سطح قدرت انجام نشد", + "Failed to remove user from community": "کاربر از اجتماع حذف نشد", + "Failed to withdraw invitation": "دعوت پس گرفته نشد", + "Remove this user from community?": "این کاربر از اجتماع حذف شود؟", + "Disinvite this user from community?": "دعوت این کاربر به اجتماع لغو شود؟", + "Remove from community": "حذف از اجتماع", + "Unmute": "صدادار", + "Failed to mute user": "کاربر بی صدا نشد", + "Ban this user?": "کاربر تحریم شود؟", + "Unban this user?": "لغو تحریم این کاربر؟", + "Ban": "تحریم", + "Remove recent messages": "حذف پیام‌های اخیر", + "Remove %(count)s messages|one": "حذف ۱ پیام", + "Remove %(count)s messages|other": "حذف %(count)s پیام", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "برای مقدار زیادی پیام ممکن است مدتی طول بکشد. لطفا در این بین مرورگر خود را refresh نکنید.", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "شما در شرف حذف ۱ پیام از کاربر %(user)s هستید. این قابل بازگشت نیست. آیا مایل هستید ادامه دهید؟", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "شما در شرف حذف %(count)s پیام از کاربر %(user)s هستید. این قابل بازگشت نیست. آیا مایل هستید ادامه دهید؟", + "Remove recent messages by %(user)s": "حذف پیام‌های اخیر %(user)s", + "Try scrolling up in the timeline to see if there are any earlier ones.": "در پیام‌ها بالا بروید تا ببینید آیا موارد قدیمی وجود دارد یا خیر.", + "No recent messages by %(user)s found": "هیچ پیام جدیدی برای %(user)s یافت نشد", + "Failed to kick": "اخراج انجام نشد", + "Kick this user?": "اخراج این کاربر؟", + "Disinvite this user?": "عدم دعوت این کاربر؟", + "Kick": "اخراج", + "Demote": "تنزل رتبه", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "شما نمی توانید این تغییر را لغو کنید زیرا در حال تنزل خود هستید، اگر آخرین کاربر ممتاز در اتاق باشید بازپس گیری امتیازات غیرممکن است.", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "شما نمی توانید این تغییر را لغو کنید زیرا در حال تنزل خود هستید، اگر آخرین کاربر ممتاز در فضای کاری باشید، بازپس گیری امتیازات غیرممکن است.", + "Demote yourself?": "خودتان را تنزل می‌دهید؟", + "Direct message": "پیام مستقیم", + "Share Link to User": "اشتراک لینک برای کاربر", + "Invite": "دعوت", + "Mention": "اشاره", + "Jump to read receipt": "پرش به آخرین پیام خوانده شده", + "Hide sessions": "مخفی کردن نشست‌ها", + "%(count)s sessions|one": "%(count)s نشست", + "%(count)s sessions|other": "%(count)s نشست", + "Hide verified sessions": "مخفی کردن نشست‌های تأیید شده", + "%(count)s verified sessions|one": "1 نشست تأیید شده", + "%(count)s verified sessions|other": "%(count)s نشست تایید شده", + "Not trusted": "غیرقابل اعتماد", + "Trusted": "قابل اعتماد", + "Room settings": "تنظیمات اتاق", + "Share room": "به اشتراک گذاری اتاق", + "Show files": "نمایش پرونده ها", + "%(count)s people|one": "نفر %(count)s", + "%(count)s people|other": "نفر %(count)s", + "About": "درباره", + "Not encrypted": "رمزگذاری نشده", + "Add widgets, bridges & bots": "افزودن ابزارک‌ها، پل‌ها و ربات‌ها", + "Edit widgets, bridges & bots": "ویرایش ابزارک ها ، پل ها و ربات ها", + "Widgets": "ابزارک ها", + "Set my room layout for everyone": "چیدمان اتاق من را برای همه تنظیم کن", + "Options": "گزینه ها", + "Unpin a widget to view it in this panel": "برای مشاهده ویجت در این صفحه، پین آن را بردارید", + "Unpin": "برداشتن پین", + "You can only pin up to %(count)s widgets|other": "فقط می توانید تا %(count)s ابزارک را پین کنید", + "Room Info": "اطلاعات اتاق", + "One of the following may be compromised:": "ممکن است یکی از موارد زیر به در معرض خطر باشد:", + "Yours, or the other users’ session": "نشست شما و یا کاربران دیگر", + "Yours, or the other users’ internet connection": "اتصال اینترنت شما و یا کاربران دیگر", + "The homeserver the user you’re verifying is connected to": "سروی که کاربری که شما تأیید کرده‌اید به آن متصل هستند", + "Your homeserver": "سرور شما", + "Your messages are not secure": "پیام های شما ایمن نیستند", + "For extra security, verify this user by checking a one-time code on both of your devices.": "برای امنیت بیشتر، با بررسی کد یکبارمصرف در هر دو دستگاه، این کاربر را تأیید کنید.", + "Verify User": "تأیید هویت کاربر", + "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "در اتاق‌های رمزگذاری شده، پیام‌های شما امن هستند و فقط شما و گیرنده کلیدهای منحصر به فرد برای باز کردن قفل آن‌ها را دارید.", + "Messages in this room are not end-to-end encrypted.": "پیام های موجود در این اتاق به صورت سرتاسر رمزگذاری نشده‌اند.", + "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "پیام‌های شما امن هستند و فقط شما و گیرنده کلیدهای منحصر به فرد برای باز کردن قفل آنها را دارید.", + "Failed to perform homeserver discovery": "جستجوی سرور با موفقیت انجام نشد", + "Direct Messages": "پیام مستقیم", + "You can add existing spaces to a space.": "شما می‌توانید فضاهای کاری موجود را به یک فضای کاری اضافه کنید.", + "This homeserver doesn't offer any login flows which are supported by this client.": "این سرور هیچ سازوکار ورودی را که توسط این کلاینت پشتیبانی شود، ارائه نمی‌دهد.", + "Feeling experimental?": "آیا می‌خواهید قابلیت‌های آزمایشی را تجربه کنید؟", + "Messages in this room are end-to-end encrypted.": "پیام‌های موجود در این اتاق به صورت سرتاسر رمزگذاری شده‌اند.", + "Start Verification": "شروع تایید هویت", + "Accepting…": "پذیرش…", + "Waiting for %(displayName)s to accept…": "منتظر قبول کردن توسط %(displayName)s…", + "Accept on your other login…": "در نشست دیگر خود قبول کنید…", + "Back": "بازگشت", + "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.": "هنگامی که فردی یک URL را در پیام خود قرار می دهد، می توان با مشاهده پیش نمایش آن URL، اطلاعات بیشتری در مورد آن پیوند مانند عنوان ، توضیحات و یک تصویر از وب سایت دریافت کرد.", + "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.": "در اتاق های رمزگذاری شده، مانند این اتاق، پیش نمایش URL به طور پیش فرض غیرفعال است تا اطمینان حاصل شود که سرور شما (جایی که پیش نمایش ها ایجاد می شود) نمی تواند اطلاعات مربوط به پیوندهایی را که در این اتاق مشاهده می کنید جمع آوری کند.", + "URL previews are disabled by default for participants in this room.": "پیش نمایش URL به طور پیش فرض برای شرکت کنندگان در این اتاق غیرفعال است.", + "URL previews are enabled by default for participants in this room.": "پیش نمایش URL به طور پیش فرض برای شرکت کنندگان در این اتاق فعال است.", + "You have disabled URL previews by default.": "شما به طور پیش فرض پیش نمایش url را غیر فعال کرده اید.", + "You have enabled URL previews by default.": "شما به طور پیش فرض پیش نمایش url را فعال کرده اید.", + "Publish this room to the public in %(domain)s's room directory?": "این اتاق را در فهرست اتاق %(domain)s برای عموم منتشر شود؟", + "Room avatar": "آواتار اتاق", + "Room Topic": "موضوع اتاق", + "Room Name": "نام اتاق", + "New community ID (e.g. +foo:%(localDomain)s)": "شناسه جدید اجتماع (به عنوان مثال %(localDomain)s+foo::)", + "Show %(count)s more|other": "نمایش %(count)s مورد بیشتر", + "Show %(count)s more|one": "نمایش %(count)s مورد بیشتر", + "Jump to first invite.": "به اولین دعوت بروید.", + "Jump to first unread room.": "به اولین اتاق خوانده نشده بروید.", + "List options": "لیست گزینه‌ها", + "A-Z": "حروف الفبا", + "Activity": "فعالیت", + "Sort by": "مرتب سازی بر اساس", + "Show previews of messages": "مشاهده پیش‌نمایش پیام‌ها", + "Show rooms with unread messages first": "ابتدا اتاق های با پیام خوانده نشده را نمایش بده", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s هنگام تلاش برای دسترسی به اتاق بازگردانده شد. اگر فکر می کنید این پیام را به اشتباه مشاهده می کنید ، لطفا گزارش خطا ارسال کنید.", + "Try again later, or ask a room admin to check if you have access.": "بعداً دوباره امتحان کنید، یا از مدیر اتاق بخواهید که دسترسی شما را بررسی کند.", + "%(roomName)s is not accessible at this time.": "در حال حاضر %(roomName)s قابل دسترسی نیست.", + "This room doesn't exist. Are you sure you're at the right place?": "این اتاق وجود ندارد آیا مطمئن هستید که در جای مناسب قرار دارید؟", + "%(roomName)s does not exist.": "%(roomName)s وجود ندارد.", + "%(roomName)s can't be previewed. Do you want to join it?": "پیش بینی %(roomName)s امکان پذیر نیست. آیا می خواهید به آن بپیوندید؟", + "You're previewing %(roomName)s. Want to join it?": "شما در حال پیش نمایش %(roomName)s هستید. می خواهید به آن بپیوندید؟", + "Reject & Ignore user": "رد کردن و نادیده گرفتن کاربر", + " invited you": " شما را دعوت کرد", + "Do you want to join %(roomName)s?": "آیا می خواهید ب %(roomName)s بپیوندید؟", + "Start chatting": "گپ زدن را شروع کن", + " wants to chat": " می‌خواهد چت کند", + "Do you want to chat with %(user)s?": "آیا می خواهید با %(user)s چت کنید؟", + "Share this email in Settings to receive invites directly in %(brand)s.": "برای دریافت مستقیم دعوت در %(brand)s این ایمیل را در تنظیمات به اشتراک بگذارید.", + "Use an identity server in Settings to receive invites directly in %(brand)s.": "برای دریافت مستقیم دعوت در %(brand)s یک سرور هویت‌سنجی در تنظیمات مشخص کنید.", + "This invite to %(roomName)s was sent to %(email)s": "این دعوت به %(roomName)s به %(email)s ارسال شد", + "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 ارسال شده است که با حساب شما مرتبط نیست", + "Join the discussion": "به بحث بپیوندید", + "You can still join it because this is a public room.": "هنوز می توانید به آن بپیوندید زیرا این یک اتاق عمومی است.", + "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 مشکلی پیش آمده است", + "You were banned from %(roomName)s by %(memberName)s": "شما از %(roomName)s توسط %(memberName)s محروم شدید", + "Re-join": "دوباره بپیوندید", + "Forget this room": "فراموش کردن این اتاق", + "Reason: %(reason)s": "دلیل: %(reason)s", + "You were kicked from %(roomName)s by %(memberName)s": "شما توسط %(memberName)s از %(roomName)s اخراج شدید", + "Loading room preview": "در حال بارگیری پیش نمایش اتاق", + "Sign Up": "ثبت نام", + "Join the conversation with an account": "پیوستن به گفتگو با یک حساب کاربری", + "Rejecting invite …": "رد کردن دعوت …", + "Loading …": "بارگذاری …", + "Joining room …": "در حال پیوستن به اتاق …", + "This room": "این اتاق", + "%(count)s results|one": "%(count)s نتیجه", + "%(count)s results|other": "%(count)s نتیجه", + "%(count)s results in all spaces|one": "%(count)s نتیجه در تمامی فضا‌های کاری", + "%(count)s results in all spaces|other": "%(count)s نتیجه در تمامی فضا‌های کاری", + "Use the + to make a new room or explore existing ones below": "از + برای ایجاد یک اتاق جدید استفاده کنید و یا در اتاق های موجود زیر کاوش کنید", + "Quick actions": "اقدامات سریع", + "Explore all public rooms": "کاوش در تمام اتاق‌های عمومی", + "Start a new chat": "چت جدیدی را شروع کنید", + "Can't see what you’re looking for?": "نمی توانید چیزی را که به دنبال آن می‌گردید، ببینید؟", + "Empty room": "اتاق خالی", + "Custom Tag": "برچسب سفارشی", + "Suggested Rooms": "اتاق‌های پیشنهادی", + "Historical": "تاریخی", + "System Alerts": "هشدارهای سیستم", + "Low priority": "اولویت کم", + "Explore public rooms": "کاوش در اتاق‌های عمومی", + "Explore community rooms": "کاوش در اتاق‌های اجتماع", + "You do not have permissions to add rooms to this space": "شما اجازه افزودن اتاق به این فضای کاری را ندارید", + "You do not have permissions to create new rooms in this space": "شما اجازه ایجاد اتاق جدید در این فضای کاری را ندارید", + "Add room": "افزودن اتاق", + "Rooms": "اتاق‌ها", + "Start chat": "شروع چت", + "People": "افراد", + "Favourites": "موردعلاقه‌ها", + "Invites": "دعوت‌ها", + "Open dial pad": "باز کردن صفحه شماره‌گیری", + "Start a Conversation": "شروع مکالمه", + "Video call": "تماس تصویری", + "Voice call": "تماس صوتی", + "Show Widgets": "نمایش ابزارک‌ها", + "Hide Widgets": "پنهان‌کردن ابزارک‌ها", + "Join Room": "به اتاق بپیوندید", + "(~%(count)s results)|one": "(~%(count)s نتیجه)", + "(~%(count)s results)|other": "(~%(count)s نتیجه)", + "No recently visited rooms": "اخیراً از اتاقی بازدید نشده است", + "Recently visited rooms": "اتاق‌هایی که به تازگی بازدید کرده‌اید", + "Room %(name)s": "اتاق %(name)s", + "Replying": "پاسخ دادن", + "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "دیده شده توسط %(displayName)s (%(userName)s در %(dateTime)s)", + "Seen by %(userName)s at %(dateTime)s": "دیده شده توسط %(userName)s در %(dateTime)s", + "Unknown": "ناشناخته", + "Offline": "آفلاین", + "Idle": "بلااستفاده", + "Online": "آنلاین", + "Unknown for %(duration)s": "ناشناخته به مدت %(duration)s", + "Offline for %(duration)s": "آفلاین به مدت %(duration)s", + "Idle for %(duration)s": "بلااستفاده برای مدت %(duration)s", + "Online for %(duration)s": "آنلاین برای مدت %(duration)s", + "%(duration)sd": "%(duration)s روز", + "%(duration)sh": "%(duration)s ساعت", + "%(duration)sm": "%(duration)s دقیقه", + "%(duration)ss": "%(duration)s ثانیه", + "Jump to message": "رفتن به پیام", + "Unpin Message": "لغو پین پیام", + "Pinned Messages": "پیام‌های پین شده", + "Loading...": "بارگذاری...", + "No pinned messages.": "هیچ پیام پین شده‌ای وجود ندارد.", + "This is the start of .": "این شروع است.", + "Add a photo, so people can easily spot your room.": "عکس اضافه کنید تا افراد بتوانند به راحتی اتاق شما را ببینند.", + "Invite to just this room": "فقط به این اتاق دعوت کنید", + "%(displayName)s created this room.": "%(displayName)s این اتاق را ایجاد کرده است.", + "You created this room.": "شما این اتاق را ایجاد کردید.", + "Add a topic to help people know what it is about.": "یک موضوع اضافه کنید تا به افراد کمک کنید از آنچه در آن است مطلع شوند.", + "Topic: %(topic)s ": "موضوع: %(topic)s ", + "Topic: %(topic)s (edit)": "موضوع: %(topic)s (ویرایش)", + "This is the beginning of your direct message history with .": "این ابتدای تاریخچه پیام مستقیم شما با است.", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "فقط شما دو نفر در این مکالمه حضور دارید ، مگر اینکه یکی از شما کس دیگری را به عضویت دعوت کند.", + "Code block": "بلوک کد", + "Strikethrough": "خط روی متن", + "Italics": "مورب", + "Bold": "پررنگ", + "%(seconds)ss left": "%(seconds)s ثانیه باقی‌مانده", + "You do not have permission to post to this room": "شما اجازه ارسال در این اتاق را ندارید", + "This room has been replaced and is no longer active.": "این اتاق جایگزین شده‌است و دیگر فعال نیست.", + "The conversation continues here.": "گفتگو در اینجا ادامه دارد.", + "Send a message…": "ارسال یک پیام…", + "Send an encrypted message…": "ارسال پیام رمزگذاری شده …", + "Send a reply…": "ارسال پاسخ …", + "Send an encrypted reply…": "ارسال پاسخ رمزگذاری شده …", + "Upload file": "آپلود فایل", + "Emoji picker": "انتخاب کننده شکلک", + "Send message": "ارسال پیام", + "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (سطح قدرت %(powerLevelNumber)s)", + "Invited": "دعوت شد", + "Invite to this space": "به این فضای کاری دعوت کنید", + "Invite to this community": "به این انجمن دعوت کنید", + "and %(count)s others...|one": "و یکی دیگر ...", + "and %(count)s others...|other": "و %(count)s مورد دیگر ...", + "Close preview": "بستن پیش نمایش", + "Scroll to most recent messages": "به جدیدترین پیام‌ها بروید", + "Please select the destination room for this message": "لطفاً اتاق مقصد را برای این پیام انتخاب کنید", + "Failed to send": "ارسال با خطا مواجه شد", + "Your message was sent": "پیام شما ارسال شد", + "Encrypting your message...": "رمزگذاری پیام شما ...", + "Sending your message...": "در حال ارسال پیام شما ...", + "The authenticity of this encrypted message can't be guaranteed on this device.": "صحت این پیام رمزگذاری شده در این دستگاه تضمین نمی شود.", + "Encrypted by a deleted session": "با یک نشست حذف شده رمزگذاری شده است", + "Unencrypted": "رمزگذاری نشده", + "Encrypted by an unverified session": "توسط یک نشست تأیید نشده رمزگذاری شده است", + "This message cannot be decrypted": "این پیام نمی‌تواند رمزگشایی شود", + "Export room keys": "استخراج کلیدهای اتاق", + "%(nameList)s %(transitionList)s": "%(nameList)s.%(transitionList)s", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "این فرآیند به شما این امکان را می‌دهد تا کلیدهایی را که برای رمزگشایی پیام‌هایتان در اتاق‌های رمزشده نیاز دارید، در قالب یک فایل محلی استخراج کنید. بعد از آن می‌توانید این فایل را در هر کلاینت دیگری وارد (Import) کرده و قادر به رمزگشایی و مشاهده‌ی پیام‌های رمزشده‌ی مذکور باشید.", + "Language Dropdown": "منو زبان", + "View message": "مشاهده پیام", + "Information": "اطلاعات", + "Download": "دانلود", + "Rotate Right": "چرخش به راست", + "Rotate Left": "چرخش به چپ", + "Zoom in": "بزرگنمایی", + "Zoom out": "کوچک نمایی", + "%(count)s people you know have already joined|one": "%(count)s نفر از افرادی که می شناسید قبلاً پیوسته‌اند", + "%(count)s people you know have already joined|other": "%(count)s نفر از افرادی که می شناسید قبلاً به آن پیوسته‌اند", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s عضو شامل %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "شامل %(commaSeparatedMembers)s", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "فایل استخراج‌شده به هر کسی که به آن دسترسی داشته باشد اجازه می‌دهد تا تمام پیام‌هایی که شما می‌توانید آن‌ها را ببینید، مشاهده کند؛ پس باید مراقب باشید و آن را امن نگه دارید. به این منظور، شما باید عبارت امنیتی را در زیر وارد کنید.. این عبارت برای رمزکردن داده‌های استخراج‌شده‌ی شما استفاده می‌شود. در این صورت تنها راه واردکردن (Import) این داده‌ها و مشاهده‌ی آن‌ها استفاده از همین عبارت امنیتی خواهد بود.", + "View all %(count)s members|one": "نمایش ۱ عضو", + "View all %(count)s members|other": "نمایش همه %(count)s عضو", + "expand": "گشودن", + "collapse": "بستن", + "Please create a new issue on GitHub so that we can investigate this bug.": "لطفا در GitHub یک مسئله جدید ایجاد کنید تا بتوانیم این اشکال را بررسی کنیم.", + "No results": "بدون نتیجه", + "Join": "پیوستن", + "Windows": "پنجره‌ها", + "Enter passphrase": "عبارت امنیتی را وارد کنید", + "Screens": "صفحه نمایش‌ها", + "Share your screen": "به اشتراک‌گذاری صفحه نمایش", + "Confirm passphrase": "عبارت امنیتی را تائید کنید", + "This version of %(brand)s does not support searching encrypted messages": "این نسخه از %(brand)s از جستجوی پیام های رمزگذاری شده پشتیبانی نمی کند", + "Import room keys": "واردکردن (Import) کلیدهای اتاق", + "This version of %(brand)s does not support viewing some encrypted files": "این نسخه از %(brand)s از مشاهده برخی از پرونده های رمزگذاری شده پشتیبانی نمی کند", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "این فرآیند به شما اجازه می‌دهد تا کلیدهای امنیتی را وارد (Import) کنید، کلیدهایی که قبلا از کلاینت‌های دیگر خود استخراج (Export) کرده‌اید. پس از آن شما می‌توانید هر پیامی را که کلاینت دیگر قادر به رمزگشایی آن بوده را، رمزگشایی و مشاهده کنید.", + "Use the Desktop app to search encrypted messages": "برای جستجوی میان پیام‌های رمز شده از نسخه دسکتاپ استفاده کنید", + "Use the Desktop app to see all encrypted files": "برای مشاهده همه پرونده های رمز شده از نسخه دسکتاپ استفاده کنید", + "Widget added by": "ابزارک اضافه شده توسط", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "فایل استخراج‌شده با یک عبارت امنیتی محافظت می‌شود. برای رمزگشایی فایل باید عبارت امنیتی را وارد کنید.", + "Popout widget": "بیرون انداختن ابزارک", + "This widget may use cookies.": "این ابزارک ممکن است از کوکی استفاده کند.", + "Widgets do not use message encryption.": "ابزارک ها از رمزگذاری پیام استفاده نمی کنند.", + "File to import": "فایل برای واردکردن (Import)", + "Using this widget may share data with %(widgetDomain)s.": "استفاده از این ابزارک ممکن است داده‌هایی را با %(widgetDomain)s به اشتراک بگذارد.", + "New Recovery Method": "روش بازیابی جدید", + "A new Security Phrase and key for Secure Messages have been detected.": "یک عبارت امنیتی و کلید جدید برای پیام‌رسانی امن شناسایی شد.", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "استفاده از این ابزارک ممکن است داده‌هایی را با %(widgetDomain)s و سیستم مدیریت ادغام به اشتراک بگذارد.", + "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.": "اگر روش بازیابی جدیدی را تنظیم نکرده‌اید، ممکن است حمله‌کننده‌ای تلاش کند به حساب کاربری شما دسترسی پیدا کند. لطفا گذرواژه حساب کاربری خود را تغییر داده و فورا یک روش جدیدِ بازیابی در بخش تنظیمات انتخاب کنید.", + "Widget ID": "شناسه ابزارک", + "Room ID": "شناسه اتاق", + "%(brand)s URL": "آدرس %(brand)s", + "Your theme": "پوسته شما", + "Your user ID": "شناسه کاربری شما", + "Your avatar URL": "URL آواتار شما", + "Your display name": "نام نمایشی شما", + "Any of the following data may be shared:": "هر یک از داده های زیر ممکن است به اشتراک گذاشته شود:", + "This session is encrypting history using the new recovery method.": "این نشست تاریخچه‌ی پیام‌های رمزشده را با استفاده از روش جدیدِ بازیابی، رمز می‌کند.", + "Unknown Address": "آدرس ناشناخته", + "Cancel search": "لغو جستجو", + "Quick Reactions": "واکنش سریع", + "Categories": "دسته بندی ها", + "Flags": "پرچم ها", + "Symbols": "نمادها", + "Objects": "اشیاء", + "Go to Settings": "برو به تنظیمات", + "Travel & Places": "سفر و اماکن", + "Activities": "فعالیت ها", + "Set up Secure Messages": "پیام‌رسانی امن را تنظیم کنید", + "Food & Drink": "غذا و نوشیدنی", + "Recovery Method Removed": "روش بازیابی حذف شد", + "Animals & Nature": "حیوانات و طبیعت", + "Smileys & People": "لبخند و افراد", + "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "نشست فعلی تشخیص داده که عبارت امنیتی و کلید لازم شما برای پیام‌رسانی امن حذف شده‌است.", + "Frequently Used": "متداول", + "You're not currently a member of any communities.": "شما در حال حاضر عضو هیچ اجتماعی نیستید.", + "Display your community flair in rooms configured to show it.": "عنوان شما در اجتماع را در اتاق‌هایی که برای نمایش آن پیکربندی شده‌اند، نمایش بده.", + "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "اگر این کار را به صورت تصادفی انجام دادید، می‌توانید سازوکار پیام امن را برای این نشست تنظیم کرده که باعث می‌شود تمام تاریخچه‌ی این نشست با استفاده از یک روش جدیدِ بازیابی، مجددا رمزشود.", + "Something went wrong when trying to get your communities.": "هنگام تلاش برای دریافت اجتماع‌های شما مشکلی پیش آمد.", + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "اگر متد بازیابی را حذف نکرده‌اید، ممکن است حمله‌کننده‌ای سعی در دسترسی به حساب‌کاربری شما داشته باشد. گذرواژه حساب کاربری خود را تغییر داده و فورا یک روش بازیابی را از بخش تنظیمات خود تنظیم کنید.", + "Filter community rooms": "فیلتر اتاق های اجتماع", + "Add rooms to this community": "افزودن اتاق به این اجتماع", + "Only visible to community members": "قابل مشاهده فقط برای اعضای اجتماع", + "Visible to everyone": "قابل مشاهده برای همه", + "Visibility in Room List": "قابل مشاهده بودن در لیست اتاق", + "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "قابلیت مشاهده %(roomName)s در %(groupId)s بروز نشد.", + "Message downloading sleep time(ms)": "زمان خواب بارگیری پیام (ms)", + "Something went wrong!": "مشکلی پیش آمد!", + "Failed to remove '%(roomName)s' from %(groupId)s": "حذف %(roomName)s از %(groupId)s انجام نشد", + "Toggle this dialog": "کلیک کنید", + "Failed to remove room from community": "حذف اتاق از اجتماع انجام نشد", + "Removing a room from the community will also remove it from the community page.": "حذف یک اتاق از اجتماع، آن را از صفحه اجتماع نیز حذف می کند.", + "Move autocomplete selection up/down": "انتخاب تکمیل‌کننده خودکار را بالا/پایین ببرید", + "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "آیا مطمئن هستید که می خواهید %(roomName)s را از %(groupId)s حذف کنید؟", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s عضو شدند", + "example": "مثال", + "Example": "مثال", + "Skip": "بیخیال", + "Import": "واردکردن (Import)", + "Export": "استخراج (Export)", + "Space": "فضای کاری", + "Esc": "خروج", + "Super": "فوق العاده", + "Theme added!": "پوسته اضافه شد!", + "Find a room…": "یافتن اتاق…", + "If you've joined lots of rooms, this might take a while": "اگر عضو اتاق‌های بسیار زیادی هستید، ممکن است این فرآیند مقدای به طول بیانجامد", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "مدیر سرور شما قابلیت رمزنگاری سرتاسر برای اتاق‌ها و گفتگوهای خصوصی را به صورت پیش‌فرض غیرفعال کرده‌است.", + "To link to this room, please add an address.": "برای لینک دادن به این اتاق، لطفا یک نشانی برای آن اضافه کنید.", + "Filter community members": "فیلتر اعضای اجتماع", + "Failed to load group members": "اعضای گروه بارگیری نشد", + "Can't load this message": "بارگیری این پیام امکان پذیر نیست", + "Submit logs": "ارسال لاگ‌ها", + "edited": "ویرایش شده", + "Edited at %(date)s. Click to view edits.": "ویرایش شده در %(date)s. برای مشاهده ویرایش ها کلیک کنید.", + "Click to view edits": "برای مشاهده ویرایش ها کلیک کنید", + "Edited at %(date)s": "ویرایش شده در %(date)s", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "شما در آستانه هدایت شدن به یک سایت ثالث هستید بنابراین می توانید حساب خود را برای استفاده با %(integrationsUrl)s احراز هویت کنید. آیا مایل هستید ادامه دهید؟", + "Add an Integration": "یکپارچه سازی اضافه کنید", + "This room is a continuation of another conversation.": "این اتاق ادامه گفتگوی دیگر است.", + "Click here to see older messages.": "برای دیدن پیام های قدیمی اینجا کلیک کنید.", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s آواتار اتاق را به تغییر داد", + "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s آواتار اتاق را حذف کرد.", + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s آواتار خود را در %(roomName)s تغییر داد", + "Message deleted on %(date)s": "پیام در %(date)s حذف شد", + "Message deleted by %(name)s": "پیام توسط %(name)s حذف شد", + "Message deleted": "پیغام پاک شد", + "reacted with %(shortName)s": " واکنش نشان داد با %(shortName)s", + " reacted with %(content)s": " واکنش نشان داد با %(content)s", + "Reactions": "واکنش ها", + "Show all": "نمایش همه", + "Add reaction": "افزودن واکنش", + "Error processing voice message": "خطا در پردازش پیام صوتی", + "Error decrypting video": "خطا در رمزگشایی ویدیو", + "You sent a verification request": "شما یک درخواست تأیید هویت ارسال کرده‌اید", + "%(name)s wants to verify": "%(name)s می‌خواهد تأیید هویت کند", + "Declining …": "در حال رد کردن …", + "Accepting …": "در حال پذیرش …", + "%(name)s cancelled": "%(name)s لغو کرد", + "%(name)s declined": "%(name)s رد کرد", + "You cancelled": "شما لغو کردید", + "You declined": "شما رد کردید", + "%(name)s accepted": "%(name)s پذیرفت", + "You accepted": "پذیرفتید", + "Re-request encryption keys from your other sessions.": "درخواست مجدد کلید‌های رمزنگاری از نشست‌های دیگر شما.", + "Mod": "معاون", + "Hint: Begin your message with // to start it with a slash.": "نکته: پیام خود را با // شروع کنید تا با یک اسلش شروع شود.", + "You can use /help to list available commands. Did you mean to send this as a message?": "برای لیست کردن دستورات موجود می توانید از /help استفاده کنید. آیا قصد داشتید این پیام را به عنوان متم ارسال کنید؟", + "Read Marker off-screen lifetime (ms)": "خواندن نشانگر طول عمر خارج از صفحه نمایش (میلی ثانیه)", + "Notifications on the following keywords follow rules which can’t be displayed here:": "اعلان‌های مخصوص کلمات کلیدی زیر از قوانینی پیروی می کنند که نمی توانند در اینجا نمایش داده شوند:", + "sends space invaders": "ارسال مهاجمان فضایی", + "Sends the given message with a space themed effect": "پیام داده شده را به صورت مضمون فضای کاری ارسال می کند", + "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "ارسال داده‌ها به صورت ناشناس به ما در بهبود %(brand)s کمک می‌کند. برای این مورد از کوکی استفاده می‌شود.", + "If disabled, messages from encrypted rooms won't appear in search results.": "اگر غیر فعال شود، پیام‌های اتاق‌های رمزشده در نتایج جستجوها نمایش داده نمی‌شوند.", + "Disable": "غیرفعال‌کردن", + "Currently indexing: %(currentRoom)s": "هم‌اکنون ایندکس می‌شوند: %(currentRoom)s", + "%(brand)s is securely caching encrypted messages locally for them to appear in search results:": "%(brand)s پیام‌های رمزشده را به صورت امن و محلی ذخیره کرده تا در نتایج جستجو نمایش دهد:", + "Short keyboard patterns are easy to guess": "الگوهای کوتاه صفحه کلید به راحتی قابل حدس هستند", + "Straight rows of keys are easy to guess": "ردیف کلیدهای مستقیم به راحتی قابل حدس هستند", + "Common names and surnames are easy to guess": "نام و نام خانوادگی‌های متداول به راحتی قابل حدس زدن هستند", + "Names and surnames by themselves are easy to guess": "به راحتی می توان نام و نام خانوادگی را حدس زد", + "Predictable substitutions like '@' instead of 'a' don't help very much": "جایگزین‌های قابل پیش بینی مانند '@' به جای 'a' کمک زیادی نمی کند", + "Send %(eventType)s events as you in your active room": "رویدادهای %(eventType)s هنگامی که در اتاق فعال خود هستید ارسال شود", + "Unrecognised command: %(commandText)s": "دستور نامفهوم: %(commandText)s", + "Unknown Command": "دستور ناشناس", + "Server unavailable, overloaded, or something else went wrong.": "سرور در دسترس نیست، یا حجم بار روی آن زیاد شده و یا خطای دیگری رخ داده است.", + "Server error": "خطای سرور", + "Everyone in this room is verified": "همه‌ی اعضای این اتاق تائید شده‌اند", + "This room is end-to-end encrypted": "این اتاق به صورت سرتاسر رمزشده است", + "Someone is using an unknown session": "فردی از یک نشست ناشناس استفاده می‌کند", + "You have verified this user. This user has verified all of their sessions.": "شما این کاربر را تائید کرده‌اید. این کاربر تمام نشست‌های خود را تائيد کرده‌است.", + "You have not verified this user.": "شما این کاربر را تائید نکرده‌اید.", + "This user has not verified all of their sessions.": "این کاربر هیچ‌کدام از نشست‌های خود را تائید نکرده است.", + "Phone Number": "شماره تلفن", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "یک پیام متنی به +%(msisdn)s ارسال شد. لطفا کد تائید موجود در آن را وارد کنید.", + "Remove %(phone)s?": "%(phone)s را پاک می‌کنید؟", + "Email Address": "آدرس ایمیل", + "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "جهت تائيد آدرس ایمیل، ما یک ایمیل برای شما ارسال کردیم. لطفا فرآیند موجود در ایمیل را پی گرفته و سپس بر روی دکمه‌ی زیر کلیک نمائید.", + "Unable to add email address": "امکان اضافه‌کردن آدرس ایمیل وجود ندارد", + "This doesn't appear to be a valid email address": "به نظر می‌رسد این یک آدرس ایمیل معتبر نیست", + "Invalid Email Address": "آدرس ایمیل نامعتبر", + "Remove %(email)s?": "%(email)s را پاک می‌کنید؟", + "Unable to remove contact information": "حذف اطلاعات تماس امکان‌پذیر نیست", + "Discovery options will appear once you have added a phone number above.": "امکانات کاوش و جستجو بلافاصله بعد از اضافه‌کردن شماره تلفن در بالا ظاهر خواهند شد.", + "Verification code": "کد تائید", + "Please enter verification code sent via text.": "لطفا کد تائیدی را که از طریق متن ارسال شده‌است، وارد کنید.", + "Unable to verify phone number.": "امکان تائید شماره تلفن وجود ندارد.", + "Unable to share phone number": "امکان به اشتراک‌گذاری شماره تلفن وجود ندارد", + "Unable to revoke sharing for phone number": "لغو اشتراک‌گذاری شماره تلفن امکان‌پذیر نیست", + "Discovery options will appear once you have added an email above.": "امکانات کاوش و جستجو بلافاصله بعد از اضافه‌کردن یک ایمیل در بالا ظاهر خواهند شد.", + "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "پس از فعال‌کردن رمزنگاری برای یک اتاق، امکان غیرفعال‌کردن آن وجود ندارد. پیام‌هایی که در اتاق‌های رمزشده ارسال می‌شوند، توسط سرور دیده نشده و فقط اعضای اتاق امکان مشاهده‌ی آن‌ها را دارند. فعال‌کردن رمزنگاری برای یک اتاق می‌تواند باعث از کار افتادن بسیاری از بات‌ها و پل‌های ارتباطی (bridges) شود. در مورد رمزنگاری بیشتری بدانید.", + "Send %(eventType)s events": "ارسال رخدادهای %(eventType)s", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "در تغییر الزامات سطح دسترسی اتاق خطایی رخ داد. از داشتن دسترسی‌های کافی اطمینان حاصل کرده و مجددا امتحان کنید.", + "Error changing power level requirement": "خطا در تغییر الزامات سطح دسترسی", + "Banned by %(displayName)s": "توسط %(displayName)s تحریم شد", + "Change server ACLs": "لیست‌های کنترل دسترسی (ACL) سرور را تغییر دهید", + "This room is bridging messages to the following platforms. Learn more.": "این اتاق، ارتباط بین پیام‌ها و پلتفورم‌های زیر را ایجاد می‌کند. بیشتر بدانید.", + "This room isn’t bridging messages to any platforms. Learn more.": "این اتاق پیام‌های شما را با هیچ پلتفورمی ارتباط نمی‌دهد. بیشتر بدانید.", + "View older messages in %(roomName)s.": "پیام‌های قدیمی اتاق %(roomName)s را مشاهده کنید.", + "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.": "هشدار: به‌روزرسانی یک اتاق، اعضای آن را به صورت خودکار به نسخه‌ی جدید همان اتاق اضافه نمی‌کند. ما یک لینک به نسخه‌ی جدید اتاق را در نسخه‌ی قدیمی اتاق قرار می‌دهیم - اعضای اتاق برای اضافه‌شدن به نسخه‌ی جدید اتاق باید بر روی آن لینک کلیک کنند.", + "You may need to manually permit %(brand)s to access your microphone/webcam": "ممکن است لازم باشد دسترسی %(brand)s به میکروفون/دوربین را به صورت دستی فعال کنید", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "نام نشست‌های زیر و خارج‌شدن از آن‌ها را مدیریت کرده و یا آن‌ها از در پروفایل کاربری خود تائید کنید.", + "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s داده‌های تجزیه و تحلیلی را به صورت غیرمشهود و جهت بهبود برنامه جمع‌آوری می‌کند.", + "Accept all %(invitedRooms)s invites": "همه‌ی دعوت‌های %(invitedRooms)s را قبول کن", + "Reject all %(invitedRooms)s invites": "همه‌ی دعوت‌های %(invitedRooms)s را رد کن", + "Bulk options": "گزینه‌های دسته‌جمعی", + "Read Marker lifetime (ms)": "مدت‌زمان نشانه‌ی خوانده‌شده (ms)", + "Composer": "سازنده", + "Show tray icon and minimize window to it on close": "آیکون جعبه را نشان داده و هنگام بسته‌شدن، پنجره را در قالب آن کوچک کن", + "Always show the window menu bar": "همیشه نوار فهرست پنجره را نشان بده", + "Warn before quitting": "قبل از خروج هشدا بده", + "Start automatically after system login": "پس از ورود به سیستم به صورت خودکار آغاز کن", + "Subscribe": "اضافه‌شدن", + "Room ID or address of ban list": "شناسه‌ی اتاق یا آدرس لیست تحریم", + "If this isn't what you want, please use a different tool to ignore users.": "اگر این چیزی نیست که شما می‌خواهید، از یک ابزار دیگر برای نادیده‌گرفتن کاربران استفاده نمائيد.", + "Subscribing to a ban list will cause you to join it!": "ثبت‌نام کردن در یک لیست تحریم باعث می‌شود شما هم عضو آن شوید!", + "eg: @bot:* or example.org": "برای مثال: @bot:* یا example.org", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "لیست تحریم شخصی شما همه‌ی کاربران و سرورهایی را که شما تمایلی به دیدن پیام‌های آن‌ها ندارید را در خود جای می‌دهد. بعد از نادیده‌گرفتن اولین کاربر یا سرور، یک اتاق جدید در لیست اتاق‌های شما با نام 'لیست تحریم من' نمایش داده می‌شود - برای اینکه لیست تحریم‌ها کار کند، اتاق را ترک نکنید.", + "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.": "نادیده‌گرفتن افراد توسط لیست تحریم صورت می‌گیرد که حاوی قوانینی برای تشخیص این است که چه کسی را تحریم کند. اضافه‌شدن به لیست تحریم به این معناست که کاربر/سرور بلاک شده و از دید شما پنهان خواهد بود.", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "شما می‌توانید گذرواژه‌ی خود را تغییر دهید، اما برخی از قابلیت ها تا زمان بازگشت سرور هویت‌سنجی در دسترس نخواهند بود. اگر مدام این هشدار را می‌بینید، پیکربندی خود را بررسی کرده یا با مدیر سرور تماس بگیرید.", + "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.": "کاربران و سرورهایی که قصد نادیده گرفتن آن‌ها را دارید در این‌جا اضافه کنید. در %(brand)s از ستاره (*) برای مچ‌شدن با هر کاراکتری استفاده کنید. برای مثال، @bot:* همه‌ی کاربران یا سرورهایی را که نام 'bot' در آن‌ها وجود دارد، نادیده می‌گیرد.", + "⚠ These settings are meant for advanced users.": "⚠ این تنظیمات برای کاربران حرفه‌ای قرار داده شده‌است.", + "You are currently subscribed to:": "شما هم‌اکنون مشترک شده‌اید در:", + "You are currently ignoring:": "شما در حال حاضر این موارد را نادیده گرفته‌اید:", + "Ban list rules - %(roomName)s": "قوانین لیست تحریم - %(roomName)s", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "تمایل به آزمایش‌کردن دارید؟ آزمایشگاه بهترین مکان برای دریافت چیزهای جدید، تست قابلیت‌های نو و کمک به رفع مشکلات آن‌ها قبل از انتشار نهایی است. بیشتر بدانید.", + "%(brand)s version:": "نسخه‌ی %(brand)s:", + "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "برای گزارش مشکلات امنیتی مربوط به ماتریکس، لطفا سایت Matrix.org بخش Security Disclosure Policy را مطالعه فرمائید.", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "اگر از طریق گیتهاب مشکلی را ثبت کرده‌اید، لاگ این مشکلات می‌تواند به ما در جهت کشف و حل آن‌ها کمک کند. لاگ مشکلات حاوی داده‌های مورد استفاده برنامه نظیر نام کاربری، شناسه یا نام مستعار اتاق‌ها یا فضاهای کاری که به آن‌ها سر زده‌اید و یا نام کاربری سایر کاربران می‌شود. این داده‌ها حاوی پیام‌های شما نمی‌شوند.", + "Chat with %(brand)s Bot": "گفتگو با بات %(brand)s", + "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "برای گرفتن کمک در استفاده از %(brand)s، اینجا کلید کرده یا با استفاده از دکمه‌ی زیر اقدام به شروع گفتگو با بات ما نمائید.", + "For help with using %(brand)s, click here.": "برای گرفتن کمک در استفاده از %(brand)s، اینجا کلیک کنید.", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "با شرایط و ضوایط سرویس سرور هویت‌سنجی (%(serverName)s) موافقت کرده تا بتوانید از طریق آدرس ایمیل و شماره تلفن قابل یافته‌شدن باشید.", + "Spell check dictionaries": "دیکشنری برای چک کردن املاء", + "Appearance Settings only affect this %(brand)s session.": "تنظیمات ظاهری برنامه تنها همین نشست %(brand)s را تحت تاثیر قرار می‌دهد.", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "نام فونتی که بر روی سیستم‌تان نصب است را وارد کرده و %(brand)s سعی می‌کند از آن استفاده کند.", + "Use between %(min)s pt and %(max)s pt": "از عددی بین %(min)s pt و %(max)s pt استفاده کنید", + "Custom font size can only be between %(min)s pt and %(max)s pt": "اندازه فونت دلخواه تنها می‌تواند عددی بین %(min)s pt و %(max)s pt باشد", + "New version available. Update now.": "نسخه‌ی جدید موجود است. هم‌اکنون به‌روزرسانی کنید.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "از یک مدیر پکپارچه‌سازی (%(serverName)s) برای مدیریت بات‌ها، ویجت‌ها و پک‌های استیکر استفاده کنید.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "استفاده از سرور هویت‌سنجی اختیاری است. اگر تصمیم بگیرید از سرور هویت‌سنجی استفاده نکنید، شما با استفاده از آدرس ایمیل و شماره تلفن قابل یافته‌شدن و دعوت‌شدن توسط سایر کاربران نخواهید بود.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "قطع ارتباط با سرور هویت‌سنجی به این معناست که شما از طریق ادرس ایمیل و شماره تلفن، بیش از این قابل یافته‌شدن و دعوت‌شدن توسط کاربران دیگر نیستید.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "در حال حاضر از سرور هویت‌سنجی استفاده نمی‌کنید. برای یافتن و یافته‌شدن توسط مخاطبان موجود که شما آن‌ها را می‌شناسید، یک مورد در پایین اضافه کنید.", + "Identity Server": "سرور هویت‌سنجی", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "اگر تمایل به استفاده از برای یافتن و یافته‌شدن توسط مخاطبان خود را ندارید، سرور هویت‌سنجی دیگری را در پایین وارد کنید.", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "در حال حاضر شما از برای یافتن و یافته‌شدن توسط مخاطبانی که می‌شناسید، استفاده می‌کنید. می‌توانید سرور هویت‌سنجی خود را در زیر تغییر دهید.", + "Identity Server (%(server)s)": "سرور هویت‌سنجی (%(server)s)", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "توصیه می‌کنیم آدرس‌های ایمیل و شماره تلفن‌های خود را پیش از قطع ارتباط با سرور هویت‌سنجی از روی آن پاک کنید.", + "You are still sharing your personal data on the identity server .": "شما هم‌چنان داده‌های شخصی خودتان را بر روی سرور هویت‌سنجی به اشتراک می‌گذارید.", + "Disconnect anyway": "در هر صورت قطع کن", + "wait and try again later": "صبر کرده و بعدا دوباره امتحان کنید", + "contact the administrators of identity server ": "با مدیران سرور هویت‌سنجی تماس بگیرید", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "پلاگین‌های مرورگر خود را بررسی کنید تا مبادا سرور هویت‌سنجی را بلاک کرده باشند (پلاگینی مانند Privacy Badger)", + "You should:": "شما باید:", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "شما باید قبل از قطع اتصال، داده‌های شخصی خود را از سرور هویت‌سنجی پاک کنید. متاسفانه سرور هویت‌سنجی هم‌اکنون آفلاین بوده و یا دسترسی به آن امکان‌پذیر نیست.", + "Disconnect": "قطع شو", + "Disconnect from the identity server ?": "از سرور هویت‌سنجی قطع می‌شوید؟", + "Disconnect identity server": "اتصال با سرور هویت‌سنجی را قطع کن", + "The identity server you have chosen does not have any terms of service.": "سرور هویت‌سنجی که انتخاب کرده‌اید شرایط و ضوابط سرویس ندارد.", + "Terms of service not accepted or the identity server is invalid.": "شرایط و ضوابط سرویس پذیرفته نشده و یا سرور هویت‌سنجی معتبر نیست.", + "Disconnect from the identity server and connect to instead?": "ارتباط با سرور هویت‌سنجی قطع شده و در عوض به متصل شوید؟", + "Change identity server": "تغییر سرور هویت‌سنجی", + "Checking server": "در حال بررسی سرور", + "Could not connect to Identity Server": "اتصال به سرور هیوت‌سنجی امکان پذیر نیست", + "Not a valid Identity Server (status code %(code)s)": "سرور هویت‌سنجی معتبر نیست (کد وضعیت %(code)s)", + "Identity Server URL must be HTTPS": "پروتکل آدرس سرور هویت‌سنجی باید HTTPS باشد", + "not ready": "آماده نیست", + "ready": "آماده", + "Secret storage:": "حافظه نهان:", + "in account data": "در داده‌های حساب کاربری", + "Secret storage public key:": "کلید عمومی حافظه نهان:", + "Backup key cached:": "کلید پشتیبان ذخیره شد:", + "not stored": "ذخیره نشد", + "Backup key stored:": "کلید پشتیبان ذخیره شد:", + "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "در صورت از دست رفتن دسترسی به نشست‌هایتان، از کلیدهای رمزنگاری و داده‌های حساب کاربری خود نسخه‌ی پشتیبان تهیه نمائید. کلیدهای شما توسط کلید منحضر به فرد امنیتی (Security Key) امن خواهند ماند.", + "unexpected type": "تایپ (نوع) غیرمنتظره", + "well formed": "خوش‌ساخت", + "Back up your keys before signing out to avoid losing them.": "پیش از خروج از حساب کاربری، از کلید‌های خود پشتیبان بگیرید تا آن‌ها را از دست ندهید.", + "Your keys are not being backed up from this session.": "کلید‌های شما از این نشست پشتیبان‌گیری نمی‌شود.", + "Algorithm:": "الگوریتم:", + "Backup version:": "نسخه‌ی پشتیبان:", + "This backup is trusted because it has been restored on this session": "این نسخه‌ی پشتیبان قابل اعتماد است چرا که بر روی این نشست بازیابی شد", + "Backup is not signed by any of your sessions": "نسخه پشتیبان توسط هیچ کدام از نشست‌های شما امضاء نشده‌است", + "Backup has an invalid signature from unverified session ": "نسخه پشتیبان دارای امضاء نامعتبر از نشست تائیدنشده‌ی می‌باشد", + "Backup has an invalid signature from verified session ": "نسخه‌ی پشتبان دارای امضاء نامعتبر از نشست تائیدشده‌ی می‌باشد", + "Backup has a valid signature from unverified session ": "نسخه پشتیبان دارای امضاء معتبر از نشست تائيد نشده‌ی می‌باشد", + "Backup has a valid signature from verified session ": "نسخه پشتیبان دارای امضاء معتبر از نشست تائیدشده‌ی می‌باشد", + "Backup has an invalid signature from this session": "نسخه پشتیبان دارای امضاء نامعتبر از طرف این نشست است", + "Backup has a valid signature from this session": "نسخه پشتیبان دارای امضاء معتبر از طرف این نشست است", + "Backup has a signature from unknown session with ID %(deviceId)s": "نسخه پشتیبان دارای امضاء از نشست ناشناس با شناسه‌ی %(deviceId)s است", + "Backup has a signature from unknown user with ID %(deviceId)s": "نسخه پشتیبان دارای امضاء از کاربر ناشناس با شناسه‌ی %(deviceId)s است", + "Backup has a invalid signature from this user": "نسخه پشتیبان دارای امضاء نامعتبر از این کاربر است", + "Backup has a valid signature from this user": "نسخه پشتیبان دارای امضاء معتبر از این کاربر است", + "All keys backed up": "از همه کلیدها نسخه‌ی پشتیبان گرفته شد", + "Backing up %(sessionsRemaining)s keys...": "در حال پیشیبان‌گیری کلید‌های %(sessionsRemaining)s ...", + "Connect this session to Key Backup": "این نشست را به کلید پشتیبان‌گیر متصل کن", + "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "پیش از خروج از حساب کاربری، این نشست را به کلید پشتیبان‌گیر متصل نمائید. با این کار مانع از گم‌شدن کلیدهای که فقط بر روی این نشست وجود دارند می‌شوید.", + "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "این نشست از کلیدهای شما پشتیبان‌گیری نمی‌کند، با این حال شما یک نسخه‌ی پشتیبان موجود دارید که می‌توانید آن را بازیابی کنید.", + "This session is backing up your keys. ": "این نشست در حال پشتیبان‌گیری از کلیدهای شماست. ", + "Restore from Backup": "بازیابی از نسخه‌ی پشتیبان", + "Unable to load key backup status": "امکان بارگیری و نمایش وضعیت کلید پشتیبان وجود ندارد", + "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "آیا اطمینان دارید؟ در صورتی که از کلیدهای شما به درستی پشتیبان‌گیری نشده باشد، تمام پیام‌های رمزشده‌ی خود را از دست خواهید داد.", + "Delete Backup": "پاک‌کردن نسخه پشتیبان (Backup)", + "Save": "ذخیره", + "Profile picture": "تصویر پروفایل", + "Display Name": "نام نمایشی", + "Profile": "پروفایل", + "Upgrade to your own domain": "به دامنه‌ی خودتان به روز‌رسانی کنید", + "The operation could not be completed": "امکان تکمیل عملیات وجود ندارد", + "Failed to save your profile": "ذخیره‌ی تنظیمات شما موفقیت‌آمیز نبود", + "Enable audible notifications for this session": "فعال‌سازی اعلان‌های صدادار برای این نشست", + "Show message in desktop notification": "پیام‌ها را در اعلان دسکتاپ نشان بده", + "Enable desktop notifications for this session": "فعال‌سازی اعلان‌های دسکتاپ برای این نشست", + "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "ممکن است شما آن‌ها را بر روی کلاینت دیگری به غیر از %(brand)s پیکربندی کرده باشید. شما نمی‌توانید آن موارد را بر روی %(brand)s تغییر دهید.", + "There are advanced notifications which are not shown here.": "اعلان‌های پیشرفته‌ای وجود دارد که در این‌جا نمایش داده نمی‌شود.", + "Add an email address to configure email notifications": "برای راه‌اندازی اعلان‌های ایمیلی یک آدرس ایمیل اضافه کنید", + "Clear notifications": "پاک‌کردن اعلان‌ها", + "The integration manager is offline or it cannot reach your homeserver.": "مدیر یکپارچه‌سازی‌ یا آفلاین است و یا نمی‌تواند به سرور شما متصل شود.", + "Cannot connect to integration manager": "امکان اتصال به مدیر یکپارچه‌سازی‌ها وجود ندارد", + "Connecting to integration manager...": "در حال اتصال به مدیر پکپارچه‌سازی...", + "Message search initialisation failed": "آغاز فرآیند جستجوی پیام‌ها با شکست همراه بود", + "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s نمی‌تواند پیام‌های رمزشده را به شکل امن و به صورت محلی در هنگامی که مرورگر در حال فعالیت است ذخیره کند. از %(brand)s نسخه‌ی دسکتاپ برای نمایش پیام‌های رمزشده در نتایج جستجو استفاده نمائید.", + "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s بعضی از مولفه‌های مورد نیاز برای ذخیره امن پیام‌های رمزشده به صورت محلی را ندارد. اگر تمایل به استفاده از این قابلیت دارید، یک نسخه‌ی دلخواه از %(brand)s با مولفه‌های مورد نظر بسازید.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "پیام‌های رمزشده را به صورتی محلی و امن ذخیره کرده تا در نتایج جستجو ظاهر شوند، با استفاده از %(size)s برای ذخیره‌ی پیام‌ها از اتاق‌های %(rooms)s.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "پیام‌های رمزشده را به صورتی محلی و امن ذخیره کرده تا در نتایج جستجو ظاهر شوند، با استفاده از %(size)s برای ذخیره‌ی پیام‌ها از اتاق %(rooms)s.", + "Securely cache encrypted messages locally for them to appear in search results.": "پیام‌های رمزشده را به صورتی محلی و امن ذخیره کرده تا در نتایج جستجو ظاهر شوند.", + "Manage": "مدیریت", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "به صورت جداگانه هر نشستی که با بقیه‌ی کاربران دارید را تائید کنید تا به عنوان نشست قابل اعتماد نشانه‌گذاری شود، با این کار می‌توانید به دستگاه‌های امضاء متقابل اعتماد نکنید.", + "Encryption": "رمزنگاری", + "Last seen": "آخرین بازدید", + "You'll need to authenticate with the server to confirm the upgrade.": "برای تائید ارتقاء، نیاز به احراز هویت نزد سرور خواهید داشت.", + "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)s دعوت خود را %(count)s مرتبه پس‌گرفتند", + "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "برای اینکه بتوانید بقیه‌ی نشست‌ها را تائید کرده و به آن‌ها امکان مشاهده‌ی پیام‌های رمزشده را بدهید، ابتدا باید این نشست را ارتقاء دهید. بعد از تائیدشدن، به عنوان نشست‌ّای تائید‌شده به سایر کاربران نمایش داده خواهند شد.", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s دعوت خود را رد کرد", + "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s دعوت خود را %(count)s مرتبه رد کرد", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s دعوت‌های خود را رد کردند", + "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s دعوت خود را %(count)s مرتبه رد کردند", + "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "یک عبارت امنیتی که فقط خودتان می‌دانید را وارد کنید؛ این عبارت از داده‌های شما محافظت می‌کند. برای حفظ امنیت، نباید از گذرواژه‌ی خود در اینجا استفاده کنید.", + "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "کلید امنیتی خود را در جایی امن ذخیره کنید، چرا که از آن به عنوان محافظ پیام‌های رمز‌شده‌ی شما استفاده می‌شود.", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s خارج شد و مجددا عضو شد", + "Unable to query secret storage status": "امکان جستجو و کنکاش وضعیت حافظه‌ی مخفی میسر نیست", + "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s %(count)s مرتبه خارج شد و مجددا عضو شد", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s خارج شدند و مجددا عضو شدند", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "اگر الان لغو کنید، ممکن است پیام‌ها و داده‌های رمزشده‌ی خود را در صورت خارج‌شدن از حساب‌های کاربریتان، از دست دهید.", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s %(count)s مرتبه خارج شدند و مجددا عضو شدند", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s پیوست و خارج شد", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s %(count)s مرتبه عضو شده و خارج شدند", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s عضو شدند و خارج شدند", + "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s %(count)s مرتبه عضو شده و خارج شدند", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)s خارج شد", + "%(oneUser)sleft %(count)s times|other": "%(oneUser)s %(count)s مرتبه خارج شده‌است", + "You can also set up Secure Backup & manage your keys in Settings.": "همچنین می‌توانید پشتیبان‌گیری امن را برپا کرده و کلید‌های خود را در تنظیمات مدیریت کنید.", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s خارج شدند", + "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s %(count)s مرتبه خارج شدند", + "Upgrade your encryption": "رمزنگاری خود را ارتقا دهید", + "Set a Security Phrase": "یک عبارت امنیتی تنظیم کنید", + "Confirm Security Phrase": "عبارت امنیتی را تأیید کنید", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s پیوست", + "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s %(count)s مرتبه عضو شدند", + "Save your Security Key": "کلید امنیتی خود را ذخیره کنید", + "Unable to set up secret storage": "تنظیم حافظه‌ی پنهان امکان پذیر نیست", + "Passphrases must match": "عبارات‌های امنیتی باید مطابقت داشته باشند", + "Passphrase must not be empty": "عبارت امنیتی نمی‌تواند خالی باشد", + "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s%(count)s مرتبه عضو شده‌اند", + "Unknown error": "خطای ناشناخته", + "This room is not showing flair for any communities": "این اتاق برای هیچ اجتماعی استعداد نشان نمی دهد", + "Showing flair for these communities:": "نمایش استعداد برای این اجتماع‌ها:", + "'%(groupId)s' is not a valid community ID": "«%(groupId)s» یک شناسه معتبر اجتماع نیست", + "Invalid community ID": "شناسه انجمن نامعتبر است", + "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "در به روزرسانی وضعیت این اتاق خطایی روی داد. سرور ممکن است اجازه نداده باشد و یا خطایی موقت روی داده باشد.", + "Error updating flair": "خطا در به روزرسانی", + "Show more": "نمایش بیشتر", + "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "آدرس‌های این اتاق را تنظیم کنید تا کاربران بتوانند این اتاق را از طریق سرور شما پیدا کنند (%(localDomain)s)", + "Local Addresses": "آدرس‌های محلی", + "New published address (e.g. #alias:server)": "آدرس جدید منتشر شده (به عنوان مثال #alias:server)", + "No other published addresses yet, add one below": "آدرس دیگری منتشر نشده است، در زیر اضافه کنید", + "Other published addresses:": "دیگر آدرس‌های منتشر شده:", + "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "آدرس‌های منتشرشده را می توان در هر سرور برای پیوستن به اتاق شما استفاده کرد. برای انتشار آدرس، ابتدا باید آن را به عنوان آدرس محلی تنظیم کنید.", + "Published Addresses": "آدرس‌های منتشر شده", + "Local address": "آدرس محلی", + "This room has no local addresses": "این اتاق آدرس محلی ندارد", + "not specified": "مشخص نشده", + "Main address": "آدرس اصلی", + "Error removing address": "خطا در حذف آدرس", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "هنگام حذف آدرس خطایی روی داد. ممکن است دیگر وجود نداشته باشد یا خطایی موقت روی داده باشد.", + "You don't have permission to delete the address.": "شما اجازه حذف آدرس را ندارید.", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "هنگام ایجاد آدرس خطایی روی داد. ممکن است سرور مجاز نباشد و یا اینکه خطایی موقت رخ داده باشد.", + "Error creating address": "خطا در ایجاد آدرس", + "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "در به روزرسانی آدرس های جایگزین اتاق خطایی روی داد. ممکن است سرور مجاز نباشد و یا اینکه خطایی موقت رخ داده باشد.", + "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "در به روزرسانی آدرس اصلی اتاق خطایی روی داد. ممکن است سرور مجاز نباشد و یا خطای موقتی رخ داده باشد.", + "Error updating main address": "خطا در به روزرسانی آدرس اصلی", + "Delete recording": "حذف پیام ضبط شده", + "Stop the recording": "توقف ضبط", + "Record a voice message": "ضبط پیام صوتی", + "We didn't find a microphone on your device. Please check your settings and try again.": "ما میکروفونی در دستگاه شما پیدا نکردیم. لطفاً تنظیمات خود را بررسی کنید و دوباره امتحان کنید.", + "No microphone found": "میکروفونی یافت نشد", + "We were unable to access your microphone. Please check your browser settings and try again.": "ما نتوانستیم به میکروفون شما دسترسی پیدا کنیم. لطفا تنظیمات مرورگر خود را بررسی کنید و دوباره سعی کنید.", + "Unable to access your microphone": "دسترسی به میکروفن شما امکان پذیر نیست", + "Mark all as read": "همه را به عنوان خوانده شده علامت بزن", + "Jump to first unread message.": "رفتن به اولین پیام خوانده نشده.", + "Invited by %(sender)s": "دعوت شده توسط %(sender)s", + "Revoke invite": "لغو دعوت", + "Admin Tools": "ابزارهای مدیریت", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "دعوت لغو نشد. ممکن است سرور با یک مشکل موقتی روبرو شده باشد و یا اینکه شما مجوز کافی برای لغو دعوت را نداشته باشید.", + "Failed to revoke invite": "دعوت لغو نشد", + "Show Stickers": "نمایش استیکرها", + "Hide Stickers": "پنهان کردن استیکر‌ها", + "Stickerpack": "استیکر", + "Add some now": "اکنون چندتایی اضافه کنید", + "You don't currently have any stickerpacks enabled": "شما در حال حاضر هیچ بسته برچسب فعالی ندارید", + "Failed to connect to integration manager": "اتصال به سیستم مدیریت ادغام انجام نشد", + "Only room administrators will see this warning": "فقط مدیران اتاق این هشدار را مشاهده خواهند کرد", + "This room is running room version , which this homeserver has marked as unstable.": "این اتاق از نسخه اتاق استفاده می کند، که این سرور آن را به عنوان ناپایدار علامت گذاری کرده است.", + "This room has already been upgraded.": "این اتاق قبلاً ارتقا یافته است.", + "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "با ارتقا این اتاق نسخه فعلی اتاق خاموش شده و یک اتاق ارتقا یافته به همین نام ایجاد می شود.", + "Unread messages.": "پیام های خوانده نشده.", + "%(count)s unread messages.|one": "۱ پیام خوانده نشده.", + "%(count)s unread messages.|other": "%(count)s پیام خوانده نشده.", + "%(count)s unread messages including mentions.|one": "۱ اشاره خوانده نشده.", + "%(count)s unread messages including mentions.|other": "%(count)s پیام‌های خوانده نشده از جمله اشاره‌ها.", + "Room options": "تنظیمات اتاق", + "Leave Room": "ترک اتاق", + "Invite People": "افراد را دعوت کنید", + "Favourited": "مورد علاقه", + "Forget Room": "اتاق را فراموش کن", + "Notification options": "تنظیمات اعلان", + "Mentions & Keywords": "اشاره‌ها و کلمات کلیدی", + "Use default": "استفاده از پیش‌فرض", + "Show less": "نمایش کمتر", + "Public Name": "نام عمومی", + "ID": "شناسه", + "Delete %(count)s sessions|one": "حذف %(count)s نشست", + "Delete %(count)s sessions|other": "حذف %(count)s نشست", + "Delete sessions|one": "حذف نشست", + "Delete sessions|other": "حذف نشست‌ها", + "Click the button below to confirm deleting these sessions.|one": "برای تائید حذف این نشست بر روی دکمه‌ی زیر کلیک کنید.", + "Click the button below to confirm deleting these sessions.|other": "برای تائید حذف این نشست‌ها بر روی دکمه‌ی زیر کلیک کنید.", + "Confirm deleting these sessions": "حذف این نشست‌ها را تائید کنید", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "حذف‌کردن این نشست را با استفاده از احراز هویت یکپارچه که هویت شما را ثابت می‌کند، تائید نمائید.", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "حذف‌کردن این نشست‌ها را با استفاده از احراز هویت یکپارچه که هویت شما را ثابت می‌کند، تائید نمائید.", + "Unable to load session list": "امکان بارگیری و نمایش لیست نشست‌ها ممکن نیست", + "Your homeserver does not support session management.": "سرور شما قابلیت مدیریت نشست‌ها را پشتیبانی نمی‌کند.", + "exists": "وجود دارد", + "Homeserver feature support:": "قابلیت‌های پشتیبانی‌شده سمت سرور:", + "User signing private key:": "کلید امضاء خصوصی کاربر:", + "Self signing private key:": "کلید خصوصی self-sign:", + "not found locally": "به صورت محلی یافت نشد", + "cached locally": "به صورت محلی کش شده‌است", + "Master private key:": "شاه‌کلید خصوصی:", + "not found in storage": "در حافظه یافت نشد", + "in secret storage": "بر روی حافظه نهان", + "Cross-signing private keys:": "کلیدهای خصوصی امضاء متقابل:", + "not found": "یافت نشد", + "in memory": "بر روی حافظه", + "Cross-signing public keys:": "کلیدهای عمومی امضاء متقابل:", + "Go back": "بازگشت", + "You can change this later": "می‌توانید بعدا این را تغییر دهید", + "Invite only, best for yourself or teams": "فقط با دعوتنامه، مناسب برای خودتان یا تیم‌ها یا جمع‌های خصوصی", + "Private": "خصوصی", + "Open space for anyone, best for communities": "محیط باز برای همه، مناسب برای جمع عمومی", + "Public": "عمومی", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "محیط‌ها یک روش جدید برای دسته‌بندی کاربران و اتاق‌ها هستند. برای پیوستن به یک محیط موجود، نیاز به یک دعوتنامه دارید.", + "Create a space": "ساختن یک محیط", + "Please enter a name for the space": "لطفا یک نام برای محیط وارد کنید", + "Description": "توضیحات", + "Name": "نام", + "Upload": "بارگذاری", + "Delete": "پاک‌کردن", + "Accept to continue:": "برای ادامه را بپذیرید:", + "Decline (%(counter)s)": "رد کردن (%(counter)s)", + "Your server isn't responding to some requests.": "سرور شما به بعضی درخواست‌ها پاسخ نمی‌دهد.", + "Pin": "سنجاق", + "Folder": "پوشه", + "Headphones": "هدفون", + "Anchor": "لنگر", + "Bell": "زنگ", + "Trumpet": "شیپور", + "Guitar": "گیتار", + "Ball": "توپ", + "Trophy": "کاپ", + "Rocket": "موشک", + "Aeroplane": "هواپیما", + "Bicycle": "دوچرخه", + "Train": "قطار", + "Flag": "پرچم", + "Telephone": "تلفن", + "Hammer": "چکش", + "Key": "کلید", + "Lock": "قفل", + "Scissors": "قیچی", + "Paperclip": "گیره کاغذ", + "Pencil": "مداد", + "Book": "کتاب", + "Light bulb": "لامپ روشن", + "Gift": "هدیه", + "Clock": "ساعت", + "Hourglass": "ساعت‌شنی", + "Umbrella": "چتر", + "Thumbs up": "موافق", + "Santa": "بابانوئل", + "Spanner": "آچار", + "Glasses": "عینک", + "Hat": "کلاه", + "Robot": "ربات", + "Smiley": "لبخند", + "Heart": "قلب", + "Cake": "کیک", + "Pizza": "پیتزا", + "Corn": "ذرت", + "Strawberry": "توت‌فرنگی", + "Apple": "سیب", + "Banana": "موز", + "Fire": "آتش", + "Cloud": "ابر", + "Moon": "ماه", + "Globe": "کره", + "Mushroom": "قارچ", + "Cactus": "کاکتوس", + "Tree": "درخت", + "Flower": "گل", + "Butterfly": "پروانه", + "Octopus": "اختاپوس", + "Fish": "ماهی", + "Turtle": "لاک‌پشت", + "Penguin": "پنگوئن", + "Rooster": "خروس", + "Panda": "پاندا", + "Rabbit": "خرگوش", + "Elephant": "فیل", + "Pig": "خوک", + "Unicorn": "اسب تک‌شاخ", + "Horse": "اسب", + "Lion": "شیر", + "Cat": "گربه", + "Dog": "سگ", + "To be secure, do this in person or use a trusted way to communicate.": "برای حفظ امنیت، خودتان این کار را انجام دهید و یا از یک روش ارتباطی قابل اعتماد استفاده نمائید.", + "They don't match": "مطابقت ندارند", + "They match": "مطابقت دارند", + "Cancelling…": "در حال لغو…", + "Waiting for %(displayName)s to verify…": "منتظر %(displayName)s برای تائید کردن…", + "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "منتظر نشست دیگر شما، %(deviceName)s (%(deviceId)s) برای تائید کردن…", + "Waiting for your other session to verify…": "منتظر نشست دیگر شما برای تائید‌کردن…", + "Unable to find a supported verification method.": "روش پشتیبانی‌شده‌ای برای تائید پیدا نشد.", + "Verify this user by confirming the following number appears on their screen.": "در صورتی که عدد بعدی بر روی صفحه‌ی کاربر نمایش داده می‌شود، او را تائید نمائید.", + "Verify this session by confirming the following number appears on its screen.": "در صورتی که شماره‌ی بعدی بر روی دستگاه نمایش داده می‌شود، این نشست را تائید نمائید.", + "Verify this user by confirming the following emoji appear on their screen.": "در صورتی که همه‌ی شکلک‌های موجود بر روی صفحه‌ی دستگاه کاربر ظاهر شده‌اند، او را تائید نمائید.", + "Confirm the emoji below are displayed on both sessions, in the same order:": "تائید کنید که شکلک‌های زیر بر روی هر دو دستگاه و به ترتیب نمایش داده می‌شوند:", + "Start": "شروع", + "Compare a unique set of emoji if you don't have a camera on either device": "اگر بر روی دستگاه خود دوربین ندارید، از تطابق شکلک‌های منحصر به فرد استفاده نمائید", + "Compare unique emoji": "شکلک‌های منحصر به فرد را مقایسه کنید", + "Scan this unique code": "این QR-code منحصر به فرد را اسکن کنید", + "or": "یا", + "Verify this session by completing one of the following:": "این نشست را با دنبال‌کردن یکی از فرآیندهای زیر تائید کنید:", + "Got It": "متوجه شدم", + "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "پیام‌های رد و بدل شده با این کاربر به صورت سرتاسر رمزشده و هیچ نفر سومی امکان مشاهده و خواندن آن‌ها را ندارد.", + "You've successfully verified this user.": "شما با موفقیت این کاربر را تائید کردید.", + "Verified!": "تائید شد!", + "The other party cancelled the verification.": "طرف مقابل فرآیند تائید را لغو کرد.", + "Play": "اجرا کردن", + "Pause": "متوقف‌کردن", + "Accept": "پذیرفتن", + "Decline": "رد کردن", + "Incoming call": "تماس ورودی", + "Incoming video call": "تماس تصویری ورودی", + "Incoming voice call": "تماس صوتی ورودی", + "Unknown caller": "تماس‌گیرنده‌ی ناشناس", + "Dial pad": "صفحه شماره‌گیری", + "There was an error looking up the phone number": "هنگام یافتن شماره تلفن خطایی رخ داد", + "Unable to look up phone number": "امکان یافتن شماره تلفن میسر نیست", + "%(name)s on hold": "%(name)s در حال تعلیق است", + "Return to call": "بازگشت به تماس", + "Fill Screen": "صفحه را پر کن", + "Voice Call": "تماس صوتی", + "Video Call": "تماس تصویری", + "Connecting": "در حال اتصال", + "%(peerName)s held the call": "%(peerName)s تماس را به حالت تعلیق درآورد", + "You held the call Resume": "شما تماس را به حالت تعلیق نگه داشته‌اید ادامه", + "You held the call Switch": "شما تماس را به حالت تعلیق نگه داشته‌اید تعویض", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "با %(transferTarget)s مشورت کنید. انتقال به %(transferee)s", + "unknown person": "فرد ناشناس", + "sends confetti": "انیمیشن بارش کاغذ شادی را ارسال کن", + "sends snowfall": "انیمیشن بارش برف را ارسال کن", + "Sends the given message with snowfall": "این پیام را با انیمیشن بارش برف ارسال کن", + "sends fireworks": "انیمیشن آتش‌بازی را ارسال کن", + "Sends the given message with fireworks": "این پیام را با انیمیشن آتش‌بازی ارسال کن", + "Sends the given message with confetti": "این پیام را با انیمیشن بارش کاغد شادی ارسال کن", + "This is your list of users/servers you have blocked - don't leave the room!": "این لیست کاربران/اتاق‌هایی است که شما آن‌ها را بلاک کرده‌اید - اتاق را ترک نکنید!", + "My Ban List": "لیست تحریم‌های من", + "When rooms are upgraded": "زمانی که اتاق‌ها به‌روزرسانی می‌گردند", + "Encrypted messages in group chats": "پیام‌های رمزشده در اتاق‌ها", + "Encrypted messages in one-to-one chats": "پیام‌های رمزشده در گفتگو‌های خصوصی", + "Messages containing @room": "پیام‌های حاوی شناسه‌ی اتاق", + "Messages containing my username": "پیام‌های حاوی نام کاربری من", + "Downloading logs": "در حال دریافت لاگ‌ها", + "Uploading logs": "در حال بارگذاری لاگ‌ها", + "Show chat effects (animations when receiving e.g. confetti)": "نمایش قابلیت‌های بصری (انیمیشن‌هایی مثل بارش برف یا کاغذ شادی هنگام دریافت پیام)", + "IRC display name width": "عرض نمایش نام‌های IRC", + "Manually verify all remote sessions": "به صورت دستی همه‌ی نشست‌ها را تائید نمائید", + "How fast should messages be downloaded.": "پیام‌ها باید چقدر سریع بارگیری شوند.", + "Enable message search in encrypted rooms": "فعال‌سازی قابلیت جستجو در اتاق‌های رمزشده", + "Show previews/thumbnails for images": "پیش‌نمایش تصاویر را نشان بده", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "زمانی که سرور شما پیشنهادی ندارد، از سرور کمکی turn.hivaa.im برای برقراری تماس استفاده کنید (در این صورت آدرس IP شما برای سرور turn.hivaa.im آشکار خواهد شد)", + "Low bandwidth mode": "حالت پهنای باند کم", + "Show hidden events in timeline": "نمایش رخدادهای مخفی در گفتگو‌ها", + "Show shortcuts to recently viewed rooms above the room list": "نمایش میانبر در بالای لیست اتاق‌ها برای مشاهده‌ی اتاق‌هایی که اخیرا باز کرده‌اید", + "Show rooms with unread notifications first": "اتاق‌های با پیام‌های خوانده‌نشده را ابتدا نشان بده", + "Order rooms by name": "مرتب‌کردن اتاق‌ها بر اساس نام", + "Show developer tools": "نمایش ابزار توسعه‌دهندگان", + "Prompt before sending invites to potentially invalid matrix IDs": "قبل از ارسال دعوت‌نامه برای کاربری که شناسه‌ی او احتمالا معتبر نیست، هشدا بده", + "Enable widget screenshots on supported widgets": "فعال‌سازی امکان اسکرین‌شات برای ویجت‌های پشتیبانی‌شده", + "Room Colour": "رنگ اتاق", + "Enable URL previews by default for participants in this room": "امکان پیش‌نمایش URL را به صورت پیش‌فرض برای اعضای این اتاق فعال کن", + "Enable URL previews for this room (only affects you)": "فعال‌سازی پیش‌نمایش URL برای این اتاق (تنها شما را تحت تاثیر قرار می‌دهد)", + "Enable inline URL previews by default": "فعال‌سازی پیش‌نمایش URL به صورت پیش‌فرض", + "Never send encrypted messages to unverified sessions in this room from this session": "هرگز از این نشست، پیام‌های رمزشده برای به نشست‌های تائید نشده در این اتاق ارسال مکن", + "Never send encrypted messages to unverified sessions from this session": "هرگز از این نشست، پیام‌های رمزشده را به نشست‌های تائید نشده ارسال مکن", + "Send analytics data": "ارسال داده‌های تجزیه و تحلیلی", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "اجازه برقراری تماس‌های یک به یک را بده (با فعال‌شدن این قابلیت، ممکن است طرف مقابل بتواند آدرس IP شما را ببیند)", + "System font name": "نام فونت سیستمی", + "Use a system font": "استفاده از یک فونت موجود بر روی سیستم شما", + "Match system theme": "با پوسته‌ی سیستم تطبیق پیدا کن", + "Enable Community Filter Panel": "پنل پالایش فضای کاری را فعال کن", + "Mirror local video feed": "تصویر خودتان را هنگام تماس تصویری برعکس (مثل آینه) نمایش بده", + "Automatically replace plain text Emoji": "متن ساده را به صورت خودکار با شکلک جایگزین کن", + "Use Ctrl + Enter to send a message": "استفاده از Ctrl + Enter برای ارسال پیام", + "Use Command + Enter to send a message": "استفاده از Command + Enter برای ارسال پیام", + "Use Ctrl + F to search": "استفاده از Ctrl + F برای جستجو", + "Use Command + F to search": "استفاده از Command + F برای جستجو", + "Show typing notifications": "نمایش اعلان «در حال نوشتن»", + "Send typing notifications": "ارسال اعلان «در حال نوشتن»", + "Enable big emoji in chat": "نمایش شکلک‌های بزرگ در گفتگوها را فعال کن", + "Space used:": "فضای مصرفی:", + "Indexed messages:": "پیام‌های ایندکس‌شده:", + "Send %(eventType)s events as you in this room": "رویدادهای %(eventType)s هنگامی که داخل این اتاق هستید ارسال شود", + "Indexed rooms:": "اتاق‌های ایندکس‌شده:", + "%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s از %(totalRooms)s", + "Navigation": "پیمایش", + "Calls": "تماس‌ها", + "Room List": "لیست اتاق‌ها", + "Remain on your screen while running": "بر روی صفحه خود باقی بمانید", + "Autocomplete": "تکمیل خودکار", + "Alt": "Alt", + "Alt Gr": "Alt Gr", + "Shift": "Shift", + "Remain on your screen when viewing another room, when running": "هنگام مشاهده اتاق دیگر، روی صفحه خود باشید", + "Ctrl": "Ctrl", + "Toggle Bold": "بولد‌کردن", + "Toggle Quote": "نقل‌قول کردن", + "Toggle Italics": "ایتالیک‌کردن", + "New line": "خط جدید", + "Navigate recent messages to edit": "پیام‌های اخیر را برای ویرایش پیمایش کنید", + "Jump to start/end of the composer": "به ابتدا/انتهای سازنده پرش کن", + "Navigate composer history": "پیمایش در تاریخچه‌ی سازنده", + "Cancel replying to a message": "پاسخ به پیام را لغو کن", + "Toggle microphone mute": "میکروفون را قطع کنید", + "Toggle video on/off": "ویدئو را وصل/قطع کنید", + "Scroll up/down in the timeline": "تایم‌لاین پیام‌ها را به سمت بالا/پایین پیمایش کنید", + "Dismiss read marker and jump to bottom": "نشانه‌ی خوانده‌شده را بیخیال شو و به انتها پرش کن", + "Jump to oldest unread message": "به قدیمی‌ترین پیام خوانده نشده پرش کن", + "Upload a file": "فایل بارگذاری کنید", + "Search (must be enabled)": "جستجو (باید فعال باشد)", + "Jump to room search": "به قسمت جستجوی اتاق پرش کن", + "Navigate up/down in the room list": "در لیست اتاق‌ها به بالا/پایین بروید", + "Select room from the room list": "از لیست اتاق‌ها انتخاب کنید", + "Collapse room list section": "قسمت لیست اتاق‌ها را جمع کن", + "Expand room list section": "قسمت لیست اتاق‌ها را بسط بده", + "Clear room list filter field": "پاک‌کردن قسمت پالایش‌های لیست اتاق‌ها", + "Previous/next unread room or DM": "اتاق یا گفتگوی خوانده‌نشده قبلی/بعدی", + "Previous/next room or DM": "اتاق یا گفتگوی قبلی/بعدی", + "Toggle the top left menu": "منوی بالا سمت چپ را تغییر دهید", + "Activate selected button": "دکمه انتخاب شده را فعال کنید", + "Toggle right panel": "پانل سمت راست را تغییر دهید", + "Go to Home View": "برو به مشاهده خانه", + "Key request sent.": "درخواست کلید ارسال شد.", + "If your other sessions do not have the key for this message you will not be able to decrypt them.": "اگر بقیه‌ی نشست‌های شما نیز کلید این پیام را نداشته باشند، امکان رمزگشایی و مشاهده‌ی آن برای شما وجود نخواهد داشت.", + "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "درخواست‌های به اشتراک‌گذاری کلید برای بقیه‌ی نشست‌های شما به صورت خودکار ارسال می‌شود. اگر این درخواست‌ها را بر روی سایر نشست‌هایتان رد کرده‌اید، اینجا کلیک کنید تا درخواست به اشتراک‌گذاری کلیدها برای این نشست مجدد ارسال شود.", + "Your key share request has been sent - please check your other sessions for key share requests.": "درخواست به اشتراک‌گذاری کلید ارسال شد - لطفا بقیه‌ی نشست‌های خود را برای درخواست به اشتراک‌گذاری کلید بررسی کنید.", + "This event could not be displayed": "امکان نمایش این رخداد وجود ندارد", + "Edit message": "ویرایش پیام", + "Send as message": "ارسال به عنوان پیام", + "Show avatars in user and room mentions": "نمایش نمایه‌ها هنگام اشاره‌کردن به فرد یا اتاق", + "Jump to the bottom of the timeline when you send a message": "زمانی که پیام ارسال می‌کنید، به صورت خودکار به آخرین پیام پرش کن", + "Show line numbers in code blocks": "شماره‌ی خط‌ها را در بلاک‌های کد نمایش بده", + "Expand code blocks by default": "بلاک‌های کد را به صورت پیش‌فرض کامل نشان بده", + "Enable automatic language detection for syntax highlighting": "فعال‌سازی تشخیص خودکار زبان برای پررنگ‌سازی نحوی", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "زمان را با فرمت ۱۲ ساعته نشان بده (مثلا ۲:۳۰ بعدازظهر)", + "Show read receipts sent by other users": "نشانه‌ی خوانده‌شدن پیام توسط دیگران را نشان بده", + "Show display name changes": "تغییرات نام کاربران را نشان بده", + "Show avatar changes": "تغییرات نمایه را نشان بده", + "Show join/leave messages (invites/kicks/bans unaffected)": "پیام‌های پیوستن/ترک‌کردن را نشان بده (دعوت‌ها، اخراج‌ها و تحریم‌ها بدون اثر خواهند ماند)", + "Show a placeholder for removed messages": "جای خالی پیام‌های پاک‌شده را نشان بده", + "Use a more compact ‘Modern’ layout": "از چیدمان فشرده‌تر و مدرن استفاده کن", + "Show stickers button": "نمایش دکمه‌ی استکیر", + "Enable Emoji suggestions while typing": "پیشنهاد دادن شکلک‌ها هنگام تایپ‌کردن را فعال کن", + "Use custom size": "از اندازه‌ی دلخواه استفاده کنید", + "Font size": "اندازه فونت", + "Show info about bridges in room settings": "اطلاعات پل‌های ارتباطی را در تنظیمات اتاق نمایش بده", + "Enable advanced debugging for the room list": "فعال‌سازی قابلیت رفع‌مشکل پیشرفته برای لیست اتاق‌ها", + "Offline encrypted messaging using dehydrated devices": "ارسال پیام رمزشده به شکل آفلاین با استفاده از دستگاه‌های خاص", + "Show message previews for reactions in all rooms": "پیش‌نمایش احساسات و شکلک‌ها را برای همه اتاق‌ها نشان بده", + "Show message previews for reactions in DMs": "پیش‌نمایش احساسات و شکلک‌ها را برای گفتگوهای خصوصی نشان بده", + "Support adding custom themes": "پشتیبانی از افزودن پوسته‌های ظاهری دلخواه", + "Try out new ways to ignore people (experimental)": "روش‌های جدید برای نادیده‌گرفتن افراد را امتحان کنید (آزمایشی)", + "Multiple integration managers": "چند مدیر پکپارچه‌سازی", + "Render simple counters in room header": "شمارنده‌های ساده‌ای در سرآیند اتاق نمایش بده", + "Group & filter rooms by custom tags (refresh to apply changes)": "دسته‌بندی و پالایش اتاق‌ها با استفاده از تگ‌های دلخواه (برای اعمال تغییرات صفحه را رفرش کنید)", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "تکرارهایی مانند \"abcabcabc\" تنها مقداری سخت‌تر از \"abc\" قابل حدس‌زدن هستند", + "Add another word or two. Uncommon words are better.": "یک یا دو کلمه دیگر اضافه کنید. کلمات غیرمعمول بهتر هستند.", + "Reversed words aren't much harder to guess": "حدس زدن کلمات معکوس خیلی سخت تر نیست", + "All-uppercase is almost as easy to guess as all-lowercase": "اگر همه‌ی موارد حروف بزرگ باشند، سختی حدس‌زدن آن‌ها با حالتی که فقط از حروف کوچک استفاده شود، تفاوتی نمی‌کند", + "Capitalization doesn't help very much": "استفاده از حروه بزرگ کمک چندانی نمی‌کند", + "Avoid dates and years that are associated with you": "از تاریخ و سالهایی که با شما در ارتباط هستند خودداری کنید", + "Avoid years that are associated with you": "از سالهایی که با شما در ارتباط هستند دوری کنید", + "Avoid recent years": "از سالهای اخیر خودداری کنید", + "Avoid sequences": "از موارد پشت سر هم اجتناب کنید", + "Avoid repeated words and characters": "از تکرار کلمات و کاراکترها خودداری نمائید", + "Use a longer keyboard pattern with more turns": "از الگوی طولانی‌ و پیچیده‌تر استفاده نمائید", + "No need for symbols, digits, or uppercase letters": "نیازی به علامت ، عدد یا حروف بزرگ نیست", + "Use a few words, avoid common phrases": "از چند کلمه استفاده کنید ، از عبارات معمول خودداری نمائید", + "Unknown server error": "خطای ناشناخته از سمت سرور", + "The user's homeserver does not support the version of the room.": "سرور کاربر از نسخه‌ی اتاق پشتیبانی نمی‌کند.", + "The user must be unbanned before they can be invited.": "برای اینکه کاربر بتواند دعوت شود، ابتدا باید رفع تحریم شود.", + "User %(user_id)s may or may not exist": "کاربر %(user_id)s ممکن است وجود داشته باشد یا نداشته باشد", + "User %(user_id)s does not exist": "کاربر %(user_id)s وجود ندارد", + "User %(userId)s is already in the room": "کاربر %(userId)s هم‌اکنون عضو این اتاق است", + "You do not have permission to invite people to this room.": "شما دسترسی دعوت افراد به این اتاق را ندارید.", + "Unrecognised address": "آدرس ناشناخته", + "Error leaving room": "خطا در ترک اتاق", + "This room is used for important messages from the Homeserver, so you cannot leave it.": "این اتاق برای نمایش پیام‌های مهم سرور استفاده می‌شود، لذا امکان ترک آن وجود ندارد.", + "Can't leave Server Notices room": "نمی توان از اتاق اعلامیه های سرور خارج شد", + "Unexpected server error trying to leave the room": "خطای غیرمنتظره روی سرور هنگام تلاش برای ترک اتاق", + "Authentication check failed: incorrect password?": "احراز هویت موفقیت‌آمیز نبود: گذرواژه نادرست است؟", + "Not a valid %(brand)s keyfile": "فایل کلید %(brand)s معتبر نیست", + "Your browser does not support the required cryptography extensions": "مرورگر شما از افزونه‌های رمزنگاری مورد نیاز پشتیبانی نمی‌کند", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "%(num)s days from now": "%(num)s روز دیگر", + "about a day from now": "حدود یک روز دیگر", + "%(num)s hours from now": "%(num)s ساعت دیگر", + "about an hour from now": "حدود یک ساعت دیگر", + "%(num)s minutes from now": "%(num)s دقیقه دیگر", + "about a minute from now": "حدود یک دقیقه دیگر", + "a few seconds from now": "چند ثانیه دیگر", + "%(num)s days ago": "%(num)s روز قبل", + "about a day ago": "حدود یک روز قبل", + "%(num)s hours ago": "%(num)s ساعت قبل", + "about an hour ago": "حدود یک ساعت قبل", + "about a minute ago": "حدود یک دقیقه قبل", + "%(num)s minutes ago": "%(num)s دقیقه قبل", + "a few seconds ago": "چند ثانیه قبل", + "%(items)s and %(lastItem)s": "%(items)s و %(lastItem)s", + "%(items)s and %(count)s others|one": "%(items)s و یکی دیگر", + "%(items)s and %(count)s others|other": "%(items)s و %(count)s دیگر", + "Unable to connect to Homeserver. Retrying...": "اتصال به سرور میسر نشد. در حال تلاش دوباره...", + "Please contact your service administrator to continue using the service.": "لطفا برای ادامه‌ی استفاده از سرویس، با مدیر سرویس خود تماس بگیرید.", + "This homeserver has exceeded one of its resource limits.": "این سرور از یکی از محدودیت های منابع خود فراتر رفته است.", + "This homeserver has been blocked by its administrator.": "این سرور توسط مدیر آن مسدود شده‌است.", + "This homeserver has hit its Monthly Active User limit.": "این سرور به محدودیت بیشینه‌ی تعداد کاربران فعال ماهانه رسیده‌است.", + "Unexpected error resolving identity server configuration": "خطای غیر منتظره‌ای در حین بررسی پیکربندی سرور هویت‌سنجی رخ داد", + "Unexpected error resolving homeserver configuration": "خطای غیر منتظره‌ای در حین بررسی پیکربندی سرور رخ داد", + "No homeserver URL provided": "هیچ آدرس سروری وارد نشده‌است", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "شما می توانید وارد شوید ، اما برخی از ویژگی ها تا زمانی که سرور هویت‌سنجی آنلاین نشود ، در دسترس نخواهند بود. اگر مدام این هشدار را می بینید ، پیکربندی خود را بررسی کنید یا با مدیر سرور تماس بگیرید.", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "شما می‌توانید حساب کاربری بسازید، اما برخی قابلیت‌ها تا زمان اتصال مجدد به سرور هویت‌سنجی در دسترس نخواهند بود. اگر شما مدام این هشدار را مشاهده می‌کنید، پیکربندی خود را بررسی کرده و یا با مدیر سرور تماس بگیرید.", + "Cannot reach identity server": "دسترسی به سرور هویت‌سنجی امکان پذیر نیست", + "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.": "از مدیر %(brand)s خود بخواهید تا پیکربندی شما را از جهت ورودی‌های نادرست یا تکراری بررسی کند.", + "Kick, ban, or invite people to this room, and make you leave": "کاربران را به این اتاق دعوت کنید، آن‌ها اخراج و یا تحریم کرده، و حتی به آن‌ها اجازه دهید شما را از اتاق بیرون بیاندازند", + "Kick, ban, or invite people to your active room, and make you leave": "کاربران را به اتاق فعال خود دعوت کنید، آن‌ها اخراج و یا تحریم کرده، و حتی به آن‌ها اجازه دهید شما را از آن بیرون بیاندازند", + "End": "End", + "Enter": "Enter", + "Page Down": "Page Down", + "Page Up": "Page Up", + "Cancel autocomplete": "غیرفعال کردن تکمیل‌کننده خودکار", + "Users": "کاربران", + "Clear personal data": "پاک‌کردن داده‌های شخصی", + "You're signed out": "شما خارج شدید", + "Sign in and regain access to your account.": "وارد شوید و به حساب کاربری خود دسترسی داشته باشید.", + "Forgotten your password?": "گذرواژه‌ی خود را فراموش کردید؟", + "Enter your password to sign in and regain access to your account.": "جهت ورود مجدد به حساب کاربری و دسترسی به منوی کاربری، گذرواژه‌ی خود را وارد نمائید.", + "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "مجددا وارد حساب کاربری خود شده و کلیدهای رمزنگاری ذخیره‌شده در این نشست را بازیابی کنید. بدون آن‌ها، قادر به خواندن پیام‌های رمزشده بر روی هیچ نشست دیگری نخواهید بود.", + "Failed to re-authenticate": "احراز هویت مجدد موفیت‌آمیز نبود", + "Incorrect password": "گذرواژه صحیح نیست", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "بدون تائید نشست، به همه‌ی پیام‌هایتان دسترسی نداشته و ممکن است بقیه به شما اعتماد نکنند.", + "Your new session is now verified. Other users will see it as trusted.": "نشست جدید شما تائید شد. سایر کاربران آن را به عنوان یک نشست قابل اطمینان مشاهده می‌کنند.", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "نشست جدید شما تائید شد. اکنون به پیام‌های رمزشده‌ی شما دسترسی داشته و بقیه کاربران، آن را به عنوان یک نشست قابل اعتماد مشاهده می‌کنند.", + "Verify your identity to access encrypted messages and prove your identity to others.": "با تائید هویت خود به پیام‌های رمزشده دسترسی یافته و هویت خود را به دیگران ثابت می‌کنید.", + "Use Security Key": "استفاده از کلید امنیتی", + "Use Security Key or Phrase": "استفاده از کلید یا عبارت امنیتی", + "Decide where your account is hosted": "حساب کاربری شما بر روی کجا ساخته شود", + "Host account on": "ساختن حساب کاربری بر روی", + "Create account": "ساختن حساب کاربری", + "Registration Successful": "ثبت‌نام موفقیت‌آمیز بود", + "Already have an account? Sign in here": "حساب کاربری دارید؟ وارد شوید", + "Registration has been disabled on this homeserver.": "ثبت‌نام بر روی این سرور غیرفعال شده‌است.", + "New? Create account": "کاربر جدید هستید؟ حساب کاربری بسازید", + "Signing In...": "در حال ورود...", + "Syncing...": "در حال همگام‌سازی...", + "Set a new password": "تنظیم گذرواژه‌ی جدید", + "Return to login screen": "بازگشت به صفحه‌ی ورود", + "Your password has been reset.": "گذرواژه‌ی شما با موفقیت تغییر کرد.", + "Sign in instead": "به جای آن وارد شوید", + "Send Reset Email": "ارسال ایمیل تغییر", + "New Password": "گذرواژه جدید", + "New passwords must match each other.": "گذرواژه‌ی جدید باید مطابقت داشته باشند.", + "Please choose a strong password": "لطفا یک گذرواژه‌ی قوی انتخاب کنید", + "Session verified": "نشست تائید شد", + "Verify this login": "این ورود را تائید نمائید", + "Original event source": "منبع اصلی رخداد", + "Decrypted event source": "رمزگشایی منبع رخداد", + "Could not load user profile": "امکان نمایش پروفایل کاربر میسر نیست", + "Community and user menu": "فضای کاری و منوی کاربر", + "User menu": "منوی کاربر", + "Switch theme": "تعویض پوسته", + "Switch to dark mode": "انتخاب حالت تاریک", + "Switch to light mode": "انتخاب حالت روشن", + "User settings": "تنظیمات حساب کاربری", + "Community settings": "تنظیمات فضای کاری", + "All settings": "همه تنظیمات", + "Security & privacy": "امنیت و محرمانگی", + "Notification settings": "تنظیمات اعلان", + "Inviting...": "در حال دعوت...", + "Creating rooms...": "در حال ساختن اتاق...", + "Skip for now": "فعلا بیخیال", + "Room name": "نام اتاق", + "Support": "پشتیبانی", + "Random": "تصادفی", + "Welcome to ": "به خوش‌آمدید", + " invites you": " شما را دعوت کرد", + "Private space": "محیط خصوصی", + "Public space": "محیط عمومی", + "Spaces are a beta feature.": "فضای کاری یک قابلیت بتا است.", + "Create room": "ساختن اتاق", + "No results found": "نتیجه‌ای یافت نشد", + "Removing...": "در حال حذف...", + "You don't have permission": "شما دسترسی ندارید", + "Drop file here to upload": "برای بارگذاری فایل آن را کشیده و در این‌جا رها کنید", + "Failed to reject invite": "رد دعوتنامه با شکست همراه شد", + "Room": "اتاق", + "No more results": "نتایج بیشتری یافن نشد", + "Search failed": "جستجو موفیت‌آمیز نبود", + "You seem to be in a call, are you sure you want to quit?": "به نظر می‌رسد شما در میانه‌ی یک تماس هستید، آیا از خروج اطمینان دارید؟", + "You seem to be uploading files, are you sure you want to quit?": "به نظر می‌رسد شما در حال باگذاری فایل هستید، آیا از خروج اطمینان دارید؟", + "Connectivity to the server has been lost.": "اتصال به سرور از دست رفت.", + "Sending": "در حال ارسال", + "Retry all": "همه را دوباره امتحان کنید", + "Delete all": "حذف همه", + "Filter rooms and people": "پالایش اتاق‌ها و کاربران", + "Clear filter": "حذف پالایش", + "Filter all spaces": "پالایش همه محیط‌ها", + "Filter": "پالایش", + "Explore rooms in %(communityName)s": "جستجوی اتاق در فضای کاری %(communityName)s", + "Find a room… (e.g. %(exampleRoom)s)": "یافتن اتاق (برای مثال %(exampleRoom)s)", + "View": "مشاهده", + "Preview": "پیش‌نمایش", + "delete the address.": "آدرس را حذف کنید.", + "The homeserver may be unavailable or overloaded.": "احتمالا سرور در دسترس نباشد و یا بار زیادی روی آن قرار گرفته باشد.", + "You have no visible notifications.": "اعلان قابل مشاهده‌ای ندارید.", + "Communities are changing to Spaces": "فضای کاری به محیط تغییر پیدا کرد", + "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "برای دسته‌بندی اتاق‌ها و کاربران، فضای کاری بسازید!", + "Create a new community": "ساختن فضای کاری جدید", + "Your Communities": "فضاهای کاری شما", + "Logout": "خروج", + "Verification requested": "درخواست تائید", + "Old cryptography data detected": "داده‌های رمزنگاری قدیمی شناسایی شد", + "Review terms and conditions": "مرور شرایط و ضوابط", + "Terms and Conditions": "شرایط و ضوابط", + "Signed Out": "از حساب کاربری خارج شدید", + "Are you sure you want to leave the space '%(spaceName)s'?": "آیا از ترک فضای '%(spaceName)s' اطمینان دارید؟", + "This room is not public. You will not be able to rejoin without an invite.": "این اتاق عمومی نیست. پیوستن مجدد بدون دعوتنامه امکان‌پذیر نخواهد بود.", + "This space is not public. You will not be able to rejoin without an invite.": "این فضا عمومی نیست. امکان پیوستن مجدد بدون دعوتنامه امکان‌پذیر نخواهد بود.", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "شما در این‌جا تنها هستید. اگر اینجا را ترک کنید، دیگر هیچ‌کس حتی خودتان امکان پیوستن مجدد را نخواهید داشت.", + "You do not have permission to create rooms in this community.": "شما دسترسی کافی برای ساختن اتاق در این فضای کاری را ندارید.", + "Cannot create rooms in this community": "امکان ساختن اتاق در این اتاق میسر نیست", + "Failed to reject invitation": "رد دعوتنامه موفقیت‌آمیز نبود", + "Create a Group Chat": "ساختن یک گروه", + "Explore Public Rooms": "جستجوی اتاق‌های عمومی", + "Send a Direct Message": "ارسال یک پیام مستقیم", + "Liberate your communication": "ارتباطات خود را توسعه دهید", + "Welcome to %(appName)s": "به %(appName)s خوش‌آمدید", + "Now, let's help you get started": "همین الان شروع کنید", + "Welcome %(name)s": "%(name)s خوش‌آمدید", + "Add a photo so people know it's you.": "برای اینکه بقیه شما را بشناسند، یک تصویر اضافه کنید.", + "Great, that'll help people know it's you": "احسنت، با این کار شما به سایر افراد کمک می‌کنید که شما را بشناسند", + "Reset": "بازراه‌اندازی", + "Cross-signing is not set up.": "امضاء متقابل تنظیم نشده‌است.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "حساب کاربری شما یک هویت برای امضاء متقابل در حافظه‌ی نهان دارد، اما این هویت هنوز توسط این نشست تائید نشده‌است.", + "Cross-signing is ready for use.": "امضاء متقابل برای استفاده در دسترس است.", + "Your homeserver does not support cross-signing.": "سرور شما امضاء متقابل را پشتیبانی نمی‌کند.", + "Passwords don't match": "گذرواژه‌ها مطابقت ندارند", + "Do you want to set an email address?": "آیا تمایل به تنظیم یک ادرس ایمیل دارید؟", + "Export E2E room keys": "استخراج (Export) کلیدهای رمزنگاری اتاق‌ها", + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "در حال حاضر تغییر گذرواژه منجر به بازنشانی کلید‌های رمزنگاری سرتاسر برای همه‌ی نشست‌ها می‌شود، در نتیجه تاریخچه‌ی پیام‌های رمزشده دیگر قابل مشاهده و خواندن نیستند. لذا حتما ابتدا کلید اتاق‌های خود را Export کرده و بعد از تغییر گذرواژه، مجددا آن‌ها را Import نمائید. این فرآیند در آینده بهبود خواهد یافت.", + "Warning!": "هشدار!", + "Passwords can't be empty": "گذرواژه‌ها نمی‌توانند خالی باشند", + "New passwords don't match": "گذرواژه‌های جدید مطابقت ندارند", + "No display name": "هیچ نامی برای نمایش وجود ندارد", + "Upload new:": "بارگذاری جدید:", + "Channel: ": "کانال:", + "Workspace: ": "فضای کار:", + "This bridge is managed by .": "این پل ارتباطی توسط مدیریت می‌شود.", + "This bridge was provisioned by .": "این پل ارتباطی توسط ارائه شده‌است.", + "Space options": "گزینه‌های انتخابی محیط", + "Manage & explore rooms": "مدیریت و جستجوی اتاق‌ها", + "Add existing room": "اضافه‌کردن اتاق موجود", + "Create": "ایجاد‌کردن", + "Create new room": "ایجاد اتاق جدید", + "Leave space": "ترک محیط", + "Settings": "تنظیمات", + "Invite with email or username": "دعوت با ایمیل یا نام‌کاربری", + "Invite people": "دعوت کاربران", + "Share invite link": "به اشتراک‌گذاری لینک دعوت", + "Failed to copy": "خطا در گرفتن رونوشت", + "Copied!": "رونوشت گرفته شد!", + "Click to copy": "برای گرفتن رونوشت کلیک کنید", + "All rooms": "همه اتاق‌ها", + "Collapse space panel": "جمع‌کردن پنل محیط", + "Expand space panel": "بسط‌دادن پنل محیط", + "Creating...": "در حال ساختن...", + "You can change these anytime.": "شما می‌توانید این را هر زمان که خواستید، تغییر دهید.", + "Add some details to help people recognise it.": "برای کمک به کاربران جهت شناخت محیط، مقداری جزئیات اضافه کنید.", + "Your private space": "محیط خصوصی شما", + "Your public space": "محیط عمومی شما", + "This homeserver does not support communities": "سرور شما از قابلیت فضای کاری پشتیبانی نمی‌کند", + "Upload avatar": "بارگذاری نمایه", + "Long Description (HTML)": "توضیح طولانی (امکان استفاده از تگ‌های HTML نیز میسر است)", + "Everyone": "همه", + "Who can join this community?": "چه کسانی بتوانند به این فضای کاری بپیوندند؟", + "You are a member of this community": "شما یک عضو این فضای کاری هستید", + "You are an administrator of this community": "شما یکی از مدیران این فضای کاری هستید", + "Leave this community": "ترک این فضای کاری", + "Join this community": "پیوستن به این فضای کاری", + "Featured Users:": "کاربران ویژه:", + "Featured Rooms:": "اتاق‌های ویژه:", + "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "این اتاق‌ها به اعضای فضای کاری در صفحه‌ی این فضا نمایش داده می‌شود. اعضا‌ی فضا می‌توانند با کلیک بر روی اتاق‌ها به آن‌ها بپیوندند.", + "Community Settings": "تنظیمات فضای کاری", + "Unable to leave community": "ترک فضای کاری ممکن نیست", + "Leave Community": "ترک فضای کاری", + "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "شما مدیر این فضای کاری هستید. امکان پیوستن مجدد شما به این فضای کاری بدون دعوت یک مدیر دیگر در این فضا امکان‌پذیر نخواهد بود.", + "Unable to join community": "پیوستن به فضای کاری ممکن نیست", + "Unable to accept invite": "تائید دعوت ممکن نیست", + "Failed to update community": "به‌روزرسانی فضای کاری با موفقیت همراه نبود", + "Failed to upload image": "بارگذاری تصویر با موفقیت همراه نبود", + "Add a User": "افزودن کاربر", + "Who would you like to add to this summary?": "تمایل دارید کدام کاربران را به این خلاصه اضافه کنید؟", + "Add users to the community summary": "افزودن کاربران به خلاصه‌ی فضای کاری", + "Add a Room": "افزودن اتاق", + "Add to summary": "افزودن به خلاصه", + "Which rooms would you like to add to this summary?": "تمایل دارید چه اتاق‌هایی را به این خلاصه اضافه کنید؟", + "Add rooms to the community summary": "افزودن اتاق به خلاصه‌ی فضای کاری", + "Create community": "ساختن فضای کاری", + "Communities": "فضاهای کاری", + "Attach files from chat or just drag and drop them anywhere in a room.": "فایل‌ها را از محیط چت ضمیمه کرده و یا آن‌ها را کشیده و در محیط اتاق رها کنید.", + "No files visible in this room": "هیچ فایلی در این اتاق قابل مشاهده نیست", + "You must join the room to see its files": "برای دیدن فایل‌های یک اتاق، باید عضو آن باشید", + "Couldn't load page": "نمایش صفحه امکان‌پذیر نبود", + "Sign in with SSO": "ورود با استفاده از احراز هویت یکپارچه", + "Add an email to be able to reset your password.": "برای داشتن امکان تغییر گذرواژه در صورت فراموش‌کردن آن، لطفا یک آدرس ایمیل وارد نمائید.", + "Register": "ایجاد حساب کاربری", + "Sign in": "ورود به حساب کاربری", + "Sign in with": "نحوه ورود", + "Forgot password?": "فراموشی گذرواژه", + "Phone": "شماره تلفن", + "Username": "نام کاربری", + "That phone number doesn't look quite right, please check and try again": "به نظر شماره تلفن صحیح نمی‌باشد، لطفا بررسی کرده و مجددا تلاش فرمائید", + "Enter phone number": "شماره تلفن را وارد کنید", + "Enter email address": "آدرس ایمیل را وارد کنید", + "Enter username": "نام کاربری را وارد کنید", + "Keep going...": "ادامه دهید...", + "Nice, strong password!": "احسنت، گذرواژه‌ی انتخابی قوی است!", + "Enter password": "گذرواژه را وارد کنید", + "Start authentication": "آغاز فرآیند احراز هویت", + "Something went wrong in confirming your identity. Cancel and try again.": "تائید هویت شما با مشکل مواجه شد. لطفا فرآیند را لغو کرده و مجددا اقدام نمائید.", + "Submit": "ارسال", + "Code": "کد", + "Password": "گذرواژه", + "User Status": "وضعیت کاربر", + "This room is public": "این اتاق عمومی است", + "Avatar": "نمایه", + "Join the beta": "اضافه‌شدن به نسخه‌ی بتا", + "Leave the beta": "ترک نسخه‌ی بتا", + "Beta": "بتا", + "Tap for more info": "برای اطلاعات بیشتر کلیک کنید", + "Spaces is a beta feature": "ساختن فضا یک قابلیت بتا است", + "Move right": "به سمت راست ببر", + "Move left": "به سمت چپ ببر", + "Revoke permissions": "دسترسی‌ها را لغو کنید", + "Remove for everyone": "حذف برای همه", + "Delete widget": "حذف ویجت", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "حذف یک ویجت، باعث حذف‌شدن آن برای همه‌ی کاربران این اتاق می‌شود. آیا از حذف این ویجت اطمینان دارید؟", + "Delete Widget": "حذف ویجت", + "Take a picture": "عکس بگیرید", + "Start audio stream": "آغاز جریان صدا", + "Failed to start livestream": "آغاز livestream با شکست همراه بود", + "Unable to start audio streaming.": "شروع پخش جریان صدا امکان‌پذیر نیست.", + "View Community": "مشاهده‌ی فضای کاری", + "Set a new status...": "تنظیم وضعیت جدید...", + "Set status": "تنظیم وضعیت", + "Update status": "به‌روزرسانی وضعیت", + "Clear status": "پاک‌کردن وضعیت", + "Report Content": "گزارش محتوا", + "Share Message": "به اشتراک‌گذاری پیام", + "Share Permalink": "اشتراک لینک پیام", + "Pin Message": "پین‌کردن پیام", + "Unable to reject invite": "رد کردن دعوت امکان‌پذیر نیست", + "Reject invitation": "ردکردن دعوت", + "Hold": "نگه‌داشتن", + "Resume": "ادامه", + "Appearance": "شکل و ظاهر", + "Share": "اشتراک‌گذاری", + "Revoke": "برگرداندن", + "Complete": "تکمیل", + "Verify the link in your inbox": "لینک موجود در صندوق دریافت خود را تائید کنید", + "Unable to verify email address.": "تائید آدرس ایمیل ممکن نیست.", + "Click the link in the email you received to verify and then click continue again.": "برای تائید ادرس ایمیل، بر روی لینکی که برای شما ایمیل شده‌است کلیک کرده و مجددا بر روی ادامه کلیک کنید.", + "Your email address hasn't been verified yet": "آدرس ایمیل شما هنوز تائید نشده‌است", + "Unable to share email address": "به اشتراک‌گذاری آدرس ایمیل ممکن نیست", + "Unable to revoke sharing for email address": "لغو اشتراک گذاری برای آدرس ایمیل ممکن نیست", + "Who can access this room?": "چه افرادی بتوانند به این اتاق دسترسی داشته باشند؟", + "Encrypted": "رمزشده", + "Once enabled, encryption cannot be disabled.": "زمانی که رمزنگاری فعال شود، امکان غیرفعال‌کردن آن برای اتاق وجود ندارد.", + "Security & Privacy": "امنیت و محرمانگی", + "Who can read history?": "چه افرادی بتوانند تاریخچه اتاق را مشاهده کنند؟", + "Members only (since they joined)": "فقط اعصاء (از زمانی که به اتاق پیوسته‌اند)", + "Members only (since they were invited)": "فقط اعضاء (از زمانی که دعوت شده‌اند)", + "Members only (since the point in time of selecting this option)": "فقط اعضاء (از زمانی که این تنظیم اعمال می‌شود)", + "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "تغییر تنظیمات اینکه چه کاربرانی سابقه‌ی پیام‌ها را مشاهده کنند، تنها برای پیام‌های آتی اتاق اعمال میشود. پیام‌های قبلی متناسب با تنظیمات گذشته نمایش داده می‌شوند.", + "Only people who have been invited": "تنها کاربرانی که دعوت شده‌اند", + "Guests cannot join this room even if explicitly invited.": "کاربران مهمان حتی در صورتی که صراحتا به گروه دعوت شوند، امکان پیوستن به اتاق را نخواهند داشت.", + "Enable encryption?": "رمزنگاری را فعال می‌کنید؟", + "Select the roles required to change various parts of the room": "برای تغییر هر یک از بخش‌های اتاق، خداقل نقش مورد نیاز را انتخاب کنید", + "Permissions": "دسترسی‌ها", + "Roles & Permissions": "نقش‌ها و دسترسی‌ها", + "Muted Users": "کاربران بی‌صدا", + "Privileged Users": "کاربران ممتاز", + "No users have specific privileges in this room": "هیچ کاربری در این اتاق دسترسی خاصی ندارد", + "Notify everyone": "اعلان عمومی به همه", + "Remove messages sent by others": "پاک‌کردن پیام‌های دیگران", + "Ban users": "تحریم کاربران", + "Kick users": "اخراج کاربران", + "Change settings": "تغییر تنظیمات", + "Invite users": "دعوت کاربران", + "Send messages": "ارسال پیام‌ها", + "Default role": "نقش پیش‌فرض", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "هنگام تغییر طرح دسترسی کاربر خطایی رخ داد. از داشتن سطح دسترسی کافی برای این کار اطمینان حاصل کرده و مجددا اقدام نمائید.", + "Error changing power level": "تغییر سطح دسترسی با خطا همراه بود", + "Unban": "رفع تحریم", + "Modify widgets": "تغییر ویجت‌ها", + "Enable room encryption": "فعال‌کردن رمزنگاری برای اتاق", + "Upgrade the room": "ارتقاء نسخه اتاق", + "Change topic": "تغییر عنوان", + "Change permissions": "تغییر دسترسی‌ها", + "Change history visibility": "تغییر مشاهده‌پذیری تاریخچه", + "Change main address for the room": "تغییر آدرس اصلی اتاق", + "Change room name": "تغییر نام اتاق", + "Change room avatar": "تغییر نمایه اتاق", + "Browse": "جستجو", + "Set a new custom sound": "تنظیم صدای دلخواه جدید", + "Notification sound": "صدای اعلان", + "Sounds": "صداها", + "Uploaded sound": "صدای بارگذاری‌شده", + "Room Addresses": "آدرس‌های اتاق", + "URL Previews": "پیش‌نمایش URL", + "Bridges": "پل‌ها", + "Open Devtools": "بازکردن ابزار توسعه", + "Developer options": "گزینه‌های توسعه‌دهنده", + "Room version:": "نسخه‌ی اتاق:", + "Room version": "نسخه‌ی اتاق", + "Internal room ID:": "شناسه‌ی داخلی اتاق:", + "Room information": "اطلاعات اتاق", + "this room": "این اتاق", + "Upgrade this room to the recommended room version": "نسخه‌ی این اتاق را به نسخه‌ی توصیه‌شده ارتقاء دهید", + "This room is not accessible by remote Matrix servers": "این اتاق توسط سرورهای ماتریکس در دسترس نیست", + "Voice & Video": "صدا و تصویر", + "Audio Output": "خروجی صدا", + "No Audio Outputs detected": "هیچ خروجی صدایی یافت نشد", + "Request media permissions": "درخواست دسترسی به رسانه", + "Missing media permissions, click the button below to request.": "دسترسی به رسانه از دست رفت، برای درخواست مجدد بر روی دکمه‌ی زیر کلیک نمائید.", + "A session's public name is visible to people you communicate with": "نام عمومی یک نشست برای افرادی که با آن‌ها ارتباط برقرار کرده‌اید، قابل مشاهده است", + "Where you’re logged in": "کجا وارد حساب کاربری خود شده‌اید", + "Learn more about how we use analytics.": "در مورد نحوه‌ی استفاده‌ی ما از داده‌ها بیشتر بدانید.", + "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "به دلیل اهمیت حریم خصوصی، ما هیچ‌گونه‌ داده‌ی شخصی‌ و قابل ردگیری را از شما جمع‌آوری نمی‌کنیم.", + "Privacy": "حریم خصوصی", + "Cross-signing": "امضاء متقابل", + "Message search": "جستجوی پیام‌ها", + "Secure Backup": "پشتیبان‌گیری امن", + "You have no ignored users.": "شما هیچ کاربری را نادیده نگرفته‌اید.", + "Session key:": "کلید نشست:", + "Session ID:": "شناسه‌ی نشست:", + "Import E2E room keys": "واردکردن کلیدهای رمزنگاری اتاق‌ها", + "": "<پشتیبانی نمی‌شود>", + "Unignore": "لغو نادیده‌گرفتن", + "Autocomplete delay (ms)": "تاخیر تکمیل خودکار به میلی ثانیه", + "Timeline": "سیر زمان گفتگو‌ها", + "Room list": "لیست اتاق‌ها", + "Preferences": "ترجیحات", + "Subscribed lists": "لیست‌هایی که در آن‌ها ثبت‌نام کرده‌اید", + "Ignore": "نادیده‌گرفتن", + "Server or user ID to ignore": "شناسه‌ی سرور یا کاربر مورد نظر برای نادیده‌گرفتن", + "Personal ban list": "لیست تحریم شخصی", + "Ignored users": "کاربران نادیده‌گرفته‌شده", + "View rules": "مشاهده قوانین", + "Unsubscribe": "لغو اشتراک", + "You are not subscribed to any lists": "شما در هیچ لیستی ثبت‌نام نکرده‌اید", + "You have not ignored anyone.": "شما هیچ‌کس را نادیده نگرفته‌اید.", + "User rules": "قوانین کاربر", + "Server rules": "قوانین سرور", + "None": "هیچ‌کدام", + "Please try again or view your console for hints.": "لطفا مجددا اقدام کرده و برای کسب اطلاعات بیشتر کنسول مرورگر خود را مشاهده نمائید.", + "Error unsubscribing from list": "لغو اشتراک از لیست با خطا همراه بود", + "Error removing ignored user/server": "حذف کاربر/سرور نادیده‌گرفته‌شده با خطا همراه بود", + "Please verify the room ID or address and try again.": "لطفا شناسه یا آدرس اتاق را تائید کرده و مجددا اقدام نمائید.", + "Error subscribing to list": "ثبت‌نام در لیست با خطا همراه بود", + "Something went wrong. Please try again or view your console for hints.": "مشکلی پیش آمد. لطفا مجددا تلاش کرده و در صورت نیاز، کنسول مرورگر خود را برای کسب اطلاعات بیشتر مشاهده نمائید.", + "Error adding ignored user/server": "افزودن کاربر/سرور به لیست نادیده‌گرفته‌ها با خطا همراه بود", + "Ignored/Blocked": "نادیده گرفته‌شده/بلاک‌شده", + "Labs": "قابلیت‌های بتا", + "Clear cache and reload": "پاک‌کردن حافظه‌ی کش و راه‌اندازی مجدد", + "Copy": "رونوشت", + "Your access token gives full access to your account. Do not share it with anyone.": "توکن دسترسی شما، دسترسی کامل به حساب کاربری شما را میسر می‌سازد. لطفا آن را در اختیار فرد دیگری قرار ندهید.", + "Access Token": "توکن دسترسی", + "Identity Server is": "سرور هویت‌سنجی شما عبارت است از", + "Homeserver is": "سرور ما عبارت است از", + "olm version:": "نسخه‌ی olm:", + "Versions": "نسخه‌ها", + "Keyboard Shortcuts": "کلیدهای میانبر", + "FAQ": "سوالات پرتکرار", + "Help & About": "کمک و درباره‌ی‌ ما", + "Submit debug logs": "ارسال لاگ مشکل", + "Bug reporting": "گزارش مشکل", + "Credits": "اعتبارها", + "Legal": "قانونی", + "General": "عمومی", + "Discovery": "کاوش", + "Deactivate account": "غیرفعال‌کردن حساب کاربری", + "Deactivating your account is a permanent action - be careful!": "غیرفعال‌سازی حساب کاربری یک عمل دائمی و غیرقابل بازگشت است - لطفا مراقب باشید!", + "Account management": "مدیریت حساب کاربری", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "شما می‌توانید هر زمان که خواستید، در قسمت تنظیمات و یا با کلیک بر روی علامت بتا از محیط بتا خارج شوید.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "اگر ترک کنید، %(brand)s بعد از غیرفعال کردن قابلیت محیط بازراه‌اندازی خواهد شد. قابلیت‌های فضای کاری و تگ‌های دلخواه مجددا قابل مشاهده خواهند بود.", + "Custom user status messages": "پیام‌های وضعیت کاربر دلخواه", + "Message Pinning": "پین کردن پیام", + "New spinner design": "طرح اسپینر جدید", + "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "نسخه‌ی ۲ فضای کاری. نیاز به سرور سازگار دارد. به شدت ازمایشی و ناپایدار است - با احتباط استفاده کنید.", + "Render LaTeX maths in messages": "نمایش لاتکس ریاضیات در پیام‌ها", + "Send and receive voice messages": "ارسال و دریافت پیام‌های صوتی", + "Show options to enable 'Do not disturb' mode": "گزینه‌ها را برای فعال‌کردن حالت 'مزاحم نشوید' نشان بده", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "بازخورد شما به بهبود قابلیت محیط کمک خواهد کرد. هر چقدر وارد جزئیات بیشتری شوید، بهتر خواهد بود.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "نسخه‌ی بتا برای وب، دسکتاپ و اندروید در دسترس است. بعضی از قابلیت‌ها ممکن است بر روی سرور شما در دسترس نباشند.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s با فعال‌شدن قابلیت محیط‌ها بازراه‌اندازی خواهد شد. فضای کاری و تگ‌های دلخواه ناپدید خواهند شد.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "نسخه‌ی بتا برای کلاینت‌های وب، دسکتاپ و اندروید موجود است. از بابت امتحان‌کردن نسخه‌ی بتا سپاس‌گزاریم.", + "Spaces are a new way to group rooms and people.": "محیط‌ها روش جدیدی برای دسته‌بندی اتاق‌ها و کاربران است.", + "Spaces": "محیط‌ها", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "نمونه اولیه قابلیت محیط. با نسخه‌های ۱ و ۲ فضای کاری، و تگ‌های دلخواه سازگار نیست. برای برخی از قابلیت‌ها نیاز به سرور سازگار دارد.", + "Change notification settings": "تنظیمات اعلان را تغییر دهید", + "%(senderName)s: %(stickerName)s": "%(senderName)s:%(stickerName)s", + "%(senderName)s: %(reaction)s": "%(senderName)s:%(reaction)s", + "%(senderName)s: %(message)s": "%(senderName)s:%(message)s", + "* %(senderName)s %(emote)s": "* %(senderName)s.%(emote)s", + "%(senderName)s is calling": "%(senderName)s در حال تماس است", + "Waiting for answer": "منتظر پاسخ", + "%(senderName)s started a call": "%(senderName)s تماس را شروع کرد", + "You started a call": "شما یک تماس را شروع کردید", + "Call ended": "تماس پایان یافت", + "%(senderName)s ended the call": "%(senderName)s تماس را پایان داد", + "You ended the call": "شما تماس را پایان دادید", + "Call in progress": "تماس در جریان است", + "%(senderName)s joined the call": "%(senderName)s به تماس پیوست", + "You joined the call": "شما به تماس پیوستید", + "The person who invited you already left the room, or their server is offline.": "شخصی که شما را دعوت کرده است از اتاق خارج شده یا سرور وی در دسترس نیست.", + "The person who invited you already left the room.": "شخصی که شما را دعوت کرده از اتاق خارج شده است.", + "Please contact your homeserver administrator.": "لطفاً با مدیر سرور خود تماس بگیرید.", + "Sorry, your homeserver is too old to participate in this room.": "با عرض پوزش ، سرور شما برای شرکت در این اتاق بیش از حد قدیمی است.", + "There was an error joining the room": "هنگام پیوستن به اتاق خطایی رخ داد", + "New version of %(brand)s is available": "نسخه‌ی جدید %(brand)s وجود است", + "Update %(brand)s": "%(brand)s را به‌روزرسانی کنید", + "Check your devices": "دستگاه های خود را بررسی کنید", + "%(deviceId)s from %(ip)s": "%(deviceId)s از %(ip)s", + "New login. Was this you?": "ورود جدید. آیا شما بودید؟", + "Other users may not trust it": "ممکن است سایر کاربران به آن اعتماد نکنند", + "Safeguard against losing access to encrypted messages & data": "محافظ در برابر از دست‌دادن داده‌ها و پیام‌های رمزشده", + "Set up Secure Backup": "پشتیبان‌گیری امن را انجام دهید", + "Your homeserver has exceeded one of its resource limits.": "سرور شما از یکی از محدودیت‌های منابع خود فراتر رفته است.", + "This homeserver has been blocked by it's administrator.": "این سرور توسط مدیر آن مسدود شده است.", + "Your homeserver has exceeded its user limit.": "سرور شما از حد مجاز کاربر خود فراتر رفته است.", + "Use app": "از برنامه استفاده کنید", + "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "نسخه‌ی المنت وب برای گوشی به شکل آزمایشی است. برای داشتن تجربه‌ی بهتر و آخرین به‌روزرسانی‌ها، از برنامک موبایلی استفاده نمائید.", + "Use app for a better experience": "برای تجربه بهتر از برنامه استفاده کنید", + "Enable": "فعال کن", + "Enable desktop notifications": "فعال‌کردن اعلان‌های دسکتاپ", + "Don't miss a reply": "پاسخی را از دست ندهید", + "Review to ensure your account is safe": "برای کسب اطمینان از امن‌بودن حساب کاربری خود، لطفا بررسی فرمائید", + "You have unverified logins": "شما ورودهای تأیید نشده دارید", + "Yes": "بله", + "Help us improve %(brand)s": "به ما در بهبود %(brand)s کمک کنید", + "Unknown App": "برنامه ناشناخته", + "Share your public space": "محیط عمومی خود را به اشتراک بگذارید", + "Invite to %(spaceName)s": "دعوت به %(spaceName)s", + "A word by itself is easy to guess": "حدس زدن یک کلمه به خودی خود آسان است", + "This is similar to a commonly used password": "این مشابه یکی از گذرواژه‌‌هایی است که معمولاً استفاده می شود", + "This is a very common password": "این یک گذرواژه‌ی بسیار رایج است", + "This is a top-100 common password": "این‌ها ۱۰۰ گذرواژه‌ی پر استفاده هستند", + "This is a top-10 common password": "این‌ها ۱۰ گذرواژه‌ی پس استفاده هستند", + "Dates are often easy to guess": "حدس زدن تاریخ‌ها اغلب آسان است", + "Recent years are easy to guess": "حدس زدن سالهای اخیر آسان است", + "Sequences like abc or 6543 are easy to guess": "موارد متوالی نظیر abc یا 6543 برای حدس‌زدن راحت هستند", + "Language and region": "زبان و جغرافیا", + "Set a new account password...": "تنظیم گذرواژه جدید...", + "Phone numbers": "شماره تلفن", + "Email addresses": "آدرس ایمیل", + "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "گذرواژه‌ی شما با موفیت تغییر کرد. برای دریافت اعلان بر روی سایر نشست‌ها، لطفا مجددا وارد شوید", + "Success": "موفقیت", + "Flair": "فضای کاری", + "Customise your appearance": "ظاهر پیام‌رسان خود را سفارشی‌سازی کنید", + "Enable experimental, compact IRC style layout": "چیدمان IRC فشرده آزمایشی را فعال کنید", + "Show advanced": "نمایش بخش پیشرفته", + "Hide advanced": "پنهان‌کردن بخش پیشرفته", + "Theme": "پوسته", + "Add theme": "افزودن پوسته", + "Custom theme URL": "آدرس پوسته دلخواه", + "Error downloading theme information.": "بارگیری اطلاعات پوسته با خطا همراه بود.", + "Invalid theme schema.": "ساختار پوسته صحیح نیست.", + "Size must be a number": "سایز باید یک عدد باشد", + "Hey you. You're the best!": "سلام. حال شما خوبه؟", + "Check for update": "بررسی برای به‌روزرسانی جدید", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "مدیرهای یکپارچه‌سازی، داده‌های مربوط به پیکربندی را دریافت کرده و امکان تغییر ویجت‌ها، ارسال دعوتنامه برای اتاق و تنظیم سطح دسترسی از طرف شما را دارا هستند.", + "Manage integrations": "مدیریت پکپارچه‌سازی‌ها", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "از یک مدیر پکپارچه‌سازی برای مدیریت بات‌ها، ویجت‌ها و پک‌های استیکر مورد نظرتان استفاده نمائید.", + "Change": "تغییر بده", + "Enter a new identity server": "یک سرور هویت‌سنجی جدید وارد کنید", + "Do not use an identity server": "از سرور هویت‌سنجی استفاده نکن", + "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": "اگر این کار را انجام می‌دهید، لطفاً توجه داشته باشید که هیچ یک از پیام‌های شما حذف نمی‌شوند ، با این حال چون پیام‌ها مجددا ایندکس می‌شوند، ممکن است برای چند لحظه قابلیت جستجو با مشکل مواجه شود", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "حذف کلیدهای امضای متقابل دائمی است. هرکسی که او را تائید کرده‌باشید، هشدارهای امنیتی را مشاهده خواهد کرد. به احتمال زیاد نمی‌خواهید این کار را انجام دهید ، مگر هیچ دستگاهی برای امضاء متقابل از طریق آن نداشته باشید.", + "Enter your Security Phrase or to continue.": "برای ادامه، عبارت امنیتی خود را وارد کرده و یا .", + "Click the button below to confirm setting up encryption.": "برای تأیید و فعال‌سازی رمزگذاری ، روی دکمه زیر کلیک کنید.", + "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "دسترسی به حافظه نهان امکان‌پذیر نیست. لطفاً تأیید کنید که عبارت امنیتی صحیح را وارد کرده‌اید.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "اگر همه موارد را بازراه‌اندازی (reset) کنید، دیگر هیچ نشست تائید شده‌ای و هیچ کاربر تائيد‌ شده‌ای نخواهید داشت و ممکن است نتوانید پیام‌های گذشته‌ی خود را مشاهده نمائید.", + "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "نسخه پشتیبان با این کلید امنیتی رمزگشایی نمی شود: لطفاً بررسی کنید که کلید امنیتی درست را وارد کرده اید.", + "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "نسخه پشتیبان با این عبارت امنیتی رمزگشایی نمی‌شود: لطفاً بررسی کنید که عبارت امنیتی درست را وارد کرده اید.", + "Warning: you should only set up key backup from a trusted computer.": "هشدا: پشتیبان گیری از کلید را فقط از یک رایانه مطمئن انجام دهید.", + "Access your secure message history and set up secure messaging by entering your Security Phrase.": "با وارد کردن عبارت امنیتی خود به سابقه پیام‌های رمز شدتان دسترسی پیدا کرده و پیام امن ارسال کنید.", + "Only do this if you have no other device to complete verification with.": "این کار را فقط درصورتی انجام دهید که دستگاه دیگری برای تکمیل فرآیند تأیید ندارید.", + "Forgotten or lost all recovery methods? Reset all": "همه روش‌های بازیابی را فراموش کرده یا از دست داده‌اید؟ بازراه‌اندازی (reset) همه", + "If you've forgotten your Security Phrase you can use your Security Key or set up new recovery options": "اگر عبارت امنیتی خود را فراموش کرده اید، می توانید از کلید امنیتی خود استفاده کنید یا تنظیمات پشتیانی‌گیری را مجددا انجام دهید", + "The widget will verify your user ID, but won't be able to perform actions for you:": "ابزارک شناسه‌ی کاربری شما را تائید خواهد کرد، اما نمی‌تواند این کارها را برای شما انجام دهد:", + "This looks like a valid Security Key!": "به نظر می رسد این یک کلید امنیتی معتبر است!", + "Warning: You should only set up key backup from a trusted computer.": "هشدار: پشتیان کلید تنها باید در یک رایانه‌ی مطمئن انجام شود.", + "Allow this widget to verify your identity": "به این ابزارک اجازه دهید هویت شما را تأیید کند", + "Approve": "تایید", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "برخی از فایل‌ها برای بارگذاری بیش از حد بزرگ هستند. محدودیت اندازه فایل %(limit)s است.", + "Access your secure message history and set up secure messaging by entering your Security Key.": "با وارد کردن کلید امنیتی خود به تاریخچه‌ی پیام‌‌های رمز شده خود دسترسی پیدا کرده و پیام امن ارسال کنید.", + "These files are too large to upload. The file size limit is %(limit)s.": "این فایل‌ها برای بارگذاری بیش از حد بزرگ هستند. محدودیت اندازه فایل %(limit)s است.", + "If you've forgotten your Security Key you can ": "اگر کلید امنیتی خود را فراموش کرده‌اید ، می توانید ", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "این فایل برای بارگذاری بسیار بزرگ است. محدودیت اندازه‌ی فایل برابر %(limit)s است اما حجم این فایل %(sizeOfThisFile)s.", + "Ask this user to verify their session, or manually verify it below.": "از این کاربر بخواهید نشست خود را تأیید کرده و یا آن را به صورت دستی تأیید کنید.", + "Away": "بعدا", + "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) وارد یک نشست جدید شد بدون اینکه آن را تائید کند:", + "This homeserver would like to make sure you are not a robot.": "این سرور می خواهد مطمئن شود که شما یک ربات نیستید.", + "Confirm your identity by entering your account password below.": "با وارد کردن رمز ورود حساب خود در زیر ، هویت خود را تأیید کنید.", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "کلید عمومی captcha در پیکربندی سرور خانگی وجود ندارد. لطفاً این را به ادمین سرور خود گزارش دهید.", + "Verify your other session using one of the options below.": "نست دیگر خود را با استفاده از یکی از راهکارهای زیر تأیید کنید.", + "You signed in to a new session without verifying it:": "شما وارد یک نشست جدید شده‌اید بدون اینکه آن را تائید کنید:", + "Please review and accept all of the homeserver's policies": "لطفاً کلیه خط مشی‌های سرور را مرور و قبول کنید", + "Please review and accept the policies of this homeserver:": "لطفاً خط مشی‌های این سرور را مرور و قبول کنید:", + "Next": "بعدی", + "A confirmation email has been sent to %(emailAddress)s": "یک ایمیل تأیید به %(emailAddress)s ارسال شد", + "Document": "سند", + "Summary": "خلاصه", + "Service": "سرویس", + "Open the link in the email to continue registration.": "برای ادامه ثبت نام ، پیوند موجود در ایمیل را باز کنید.", + "Token incorrect": "کد نامعتبر است", + "To continue you need to accept the terms of this service.": "برای ادامه باید شرایط این سرویس را بپذیرید.", + "Use bots, bridges, widgets and sticker packs": "از بات‌ها، پل‌ها ارتباطی، ابزارک‌ها و بسته‌های استیکر استفاده کنید", + "A text message has been sent to %(msisdn)s": "کد فعال‌سازی به %(msisdn)s ارسال شد", + "Your browser likely removed this data when running low on disk space.": "هنگام کمبود فضای دیسک ، مرورگر شما این داده ها را حذف می کند.", + "Use an email address to recover your account": "برای بازیابی حساب خود از آدرس ایمیل استفاده کنید", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "برخی از داده‌های نشست ، از جمله کلیدهای رمزنگاری پیام‌ها موجود نیست. برای برطرف کردن این مشکل از برنامه خارج شده و مجددا وارد شوید و از کلیدها را از نسخه‌ی پشتیبان بازیابی نمائيد.", + "Enter email address (required on this homeserver)": "آدرس ایمیل را وارد کنید (در این سرور اجباری است)", + "Other users can invite you to rooms using your contact details": "سایر کاربران می توانند شما را با استفاده از اطلاعات تماستان به اتاق ها دعوت کنند", + "Enter phone number (required on this homeserver)": "شماره تلفن را وارد کنید (در این سرور اجباری است)", + "Use lowercase letters, numbers, dashes and underscores only": "فقط از حروف کوچک، اعداد، خط تیره و زیر خط استفاده کنید", + "Use email to optionally be discoverable by existing contacts.": "از ایمیل استفاده کنید تا به طور اختیاری توسط مخاطبین موجود قابل کشف باشید.", + "Use email or phone to optionally be discoverable by existing contacts.": "از ایمیل یا تلفن استفاده کنید تا به طور اختیاری توسط مخاطبین موجود قابل کشف باشید.", + "To help us prevent this in future, please send us logs.": "برای کمک به ما در جلوگیری از این امر در آینده ، لطفا لاگ‌ها را برای ما ارسال کنید.", + "You must register to use this functionality": "برای استفاده از این قابلیت باید ثبت نام کنید", + "

    HTML for your community's page

    \n

    \n Use the long description to introduce new members to the community, or distribute\n some important links\n

    \n

    \n You can even add images with Matrix URLs \n

    \n": "

    html مرتبط با اجتماع شما

    \n

    \n برای معرفی اجتماع به اعضای جدید، یا ذکر نکات مهم،\n از توضیحات مفصل استفاده کنیدلینک\n

    \n

    \n شما حتی می‌توانید تصاویر را با استفاده از url ماتریکس به این صفحه اضافه کنید \n

    \n", + "Saving...": "در حال ذخیره‌سازی...", + "Edit settings relating to your space.": "تنظیمات مربوط به فضای کاری خود را ویرایش کنید.", + "This will allow you to reset your password and receive notifications.": "با این کار می‌توانید گذرواژه خود را تغییر داده و اعلان‌ها را دریافت کنید.", + "Please check your email and click on the link it contains. Once this is done, click continue.": "لطفاً ایمیل خود را بررسی کرده و روی لینکی که برایتان ارسال شده، کلیک کنید. پس از انجام این کار، روی ادامه کلیک کنید.", + "Verification Pending": "در انتظار تائید", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "پاک کردن فضای ذخیره‌سازی مرورگر ممکن است این مشکل را برطرف کند ، اما شما را از برنامه خارج کرده و باعث می‌شود هرگونه سابقه گفتگوی رمزشده غیرقابل خواندن باشد.", + "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "اگر در گذشته از نسخه جدیدتر %(brand)s استفاده کرده‌اید ، نشست شما ممکن است با این نسخه ناسازگار باشد. این پنجره را بسته و به نسخه جدیدتر برگردید.", + "We encountered an error trying to restore your previous session.": "هنگام تلاش برای بازیابی نشست قبلی شما، با خطایی روبرو شدیم.", + "Refresh": "رفرش", + "Failed to add the following rooms to the summary of %(groupId)s:": "افزدون اتاق‌های زیر به خلاصه %(groupId)s انجام نشد:", + "You most likely do not want to reset your event index store": "به احتمال زیاد نمی‌خواهید مخزن فهرست رویدادهای خود را حذف کنید", + "Failed to remove the room from the summary of %(groupId)s": "اتاق از خلاصه %(groupId)s حذف نشد", + "Use your preferred Matrix homeserver if you have one, or host your own.": "از یک سرور مبتنی بر پروتکل ماتریکس که ترجیح می‌دهید استفاده کرده، و یا از سرور شخصی خودتان استفاده کنید.", + "The room '%(roomName)s' could not be removed from the summary.": "اتاق «%(roomName)s» از خلاصه حذف نمی شود.", + "Failed to add the following users to the summary of %(groupId)s:": "افزودن کاربران زیر به خلاصه %(groupId)s انجام نشد:", + "We call the places where you can host your account ‘homeservers’.": "ما به زیرساختی که شما می‌توانید بر روی آن حساب کاربری ایجاد کنید، \"سرور\" می‌گوییم.", + "Failed to remove a user from the summary of %(groupId)s": "حذف کاربر از خلاصه %(groupId)s انجام نشد", + "The user '%(displayName)s' could not be removed from the summary.": "کاربر «%(displayName)s» را نمی توان از خلاصه حذف کرد.", + "Leave %(groupName)s?": "%(groupName)s را ترک می‌کنید؟", + "Want more than a community? Get your own server": "بیش از یک اجتماع می خواهید؟سرور خود را دریافت کنید", + "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org بزرگترین سرور عمومی در جهان است ، بنابراین مکان خوبی برای بسیاری از افراد جهت برقراری ارتباط به شمار می‌رود.", + "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "تغییراتی که در نام و آواتار در اجتماع شما ایجاد شده است ممکن است توسط کاربران دیگر حداکثر تا ۳۰ دقیقه دیده شود.", + "Recent changes that have not yet been received": "تغییرات اخیری که هنوز دریافت نشده‌اند", + "The server is not configured to indicate what the problem is (CORS).": "سرور طوری پیکربندی نشده تا نشان دهد مشکل چیست (CORS).", + "%(inviter)s has invited you to join this community": "%(inviter)s از شما برای پیوستن به این اجتماع دعوت کرده است", + "A connection error occurred while trying to contact the server.": "هنگام تلاش برای اتصال به سرور خطایی رخ داده است.", + "Your community hasn't got a Long Description, a HTML page to show to community members.
    Click here to open settings and give it one!": "اجتماع شما دارای توصیف طولانی (صفحه HTML برای نشان دادن به اعضای انجمن) نیست.
    برای باز کردن تنظیمات اینجا کلیک کنید و یک توصیف به آن اضافه کنید!", + "Your area is experiencing difficulties connecting to the internet.": "منطقه شما در اتصال به اینترنت با مشکل روبرو است.", + "A browser extension is preventing the request.": "پلاگینی در مرورگر مانع از ارسال درخواست می‌گردد.", + "Your firewall or anti-virus is blocking the request.": "دیوار آتش یا آنتی‌ویروس شما مانع از ارسال درخواست می‌شود.", + "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "برای ادامه استفاده از سرور %(homeserverDomain)s باید شرایط و ضوابط ما را بررسی کرده و موافقت کنید.", + "The server (%(serverName)s) took too long to respond.": "زمان پاسخگویی سرور (%(serverName)s) بسیار طولانی شده‌است.", + "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "داده هایی از نسخه قدیمی %(brand)s شناسایی شده است. این امر باعث اختلال در رمزنگاری سرتاسر در نسخه قدیمی شده است. پیام های رمزگذاری شده سرتاسر که اخیراً رد و بدل شده اند ممکن است با استفاده از نسخه قدیمی رمزگشایی نشوند. همچنین ممکن است پیام های رد و بدل شده با این نسخه با مشکل مواجه شود. اگر مشکلی رخ داد، از سیستم خارج شوید و مجددا وارد شوید. برای حفظ سابقه پیام، کلیدهای خود را خروجی گرفته و دوباره وارد کنید.", + "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "سرور شما به برخی از درخواست‌ها پاسخ نمی‌دهد. در ادامه برخی از دلایل محتمل آن ذکر شده است.", + "You'll upgrade this room from to .": "این اتاق را از به ارتقا خواهید داد.", + "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "به‌روزرسانی اتاق اقدامی پیشرفته بوده و معمولاً در صورتی توصیه می‌شود که اتاق به دلیل اشکالات، فقدان قابلیت‌ها یا آسیب پذیری‌های امنیتی، پایدار و قابل استفاده نباشد.", + "Did you know: you can use communities to filter your %(brand)s experience!": "آیا می دانید: برای فیلتر کردن تجربیات خود در %(brand)s می توانید از اجتماع‌ها استفاده کنید!", + "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "این معمولاً فقط بر نحوه پردازش اتاق در سرور تأثیر می‌گذارد. اگر با %(brand)s خود مشکلی دارید، لطفاً اشکال را گزارش کنید.", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "برای تنظیم فیلتر، آواتار یک اجتماع را به صفحه فیلتر در سمت چپ صفحه بکشید. همواره می‌توانید با کلیک برر روی آواتار در صفحه فیلتر، فقط اتاق ها و افراد مرتبط با آن انجمن را مشاهده کنید.", + "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "این معمولاً فقط بر نحوه پردازش اتاق در سرور تأثیر می‌گذارد. اگر با %(brand)s خود مشکلی دارید ، لطفاً یک اشکال گزارش دهید.", + "Put a link back to the old room at the start of the new room so people can see old messages": "در ابتدای اتاق جدید پیوندی به اتاق قدیمی قرار دهید تا افراد بتوانند پیام‌های موجود در اتاق قدیمی را ببینند", + "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "از گفتگوی کاربران در نسخه قدیمی اتاق جلوگیری کرده و با ارسال پیامی به کاربران توصیه کنید به اتاق جدید منتقل شوند", + "Update any local room aliases to point to the new room": "برای اشاره به اتاق جدید، نام‌های مستعار (aliases) اتاق محلی را به‌روز کنید", + "Create a new room with the same name, description and avatar": "یک اتاق جدید با همان نام ، توضیحات و نمایه ایجاد کنید", + "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "ارتقاء این اتاق نیازمند بستن نسخه‌ی فعلی و ساختن درجای یک اتاق جدید است. برای داشتن بهترین تجربه‌ی کاربری ممکن، ما:", + "The room upgrade could not be completed": "متاسفانه فرآیند ارتقاء اتاق به پایان نرسید", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "گزارش این پیام شناسه‌ی منحصر به فرد رخداد آن را برای مدیر سرور ارسال می‌کند. اگر پیام‌های این اتاق رمزشده باشند، مدیر سرور شما امکان خواندن متن آن پیام یا مشاهده‌ی عکس یا فایل‌های دیگر را نخواهد داشت.", + "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "حواستان را جمع کنید، اگر ایمیلی اضافه نکرده و گذرواژه‌ی خود را فراموش کنید ، ممکن است دسترسی به حساب کاربری خود را برای همیشه از دست دهید.", + "Doesn't look like a valid email address": "به نظر نمی‌رسد یک آدرس ایمیل معتبر باشد", + "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s نتوانست لیست پروتکل را از سرور میزبان دریافت کند. ممکن است سرور برای پشتیبانی از شبکه های شخص ثالث خیلی قدیمی باشد.", + "If they don't match, the security of your communication may be compromised.": "اگر آنها مطابقت نداشته‌باشند ، ممکن است امنیت ارتباطات شما به خطر افتاده باشد.", + "If you didn’t sign in to this session, your account may be compromised.": "اگر وارد این نشست نشده‌اید ، ممکن است حساب کاربری شما به خطر افتاد باشد.", + "%(brand)s failed to get the public room list.": "%(brand)s نتوانست لیست اتاق‌های عمومی را دریافت کند.", + "Use this session to verify your new one, granting it access to encrypted messages:": "از این نشست برای تائید نشست‌های جدید خود و اعطای دسترسی به پیام‌های رمزشده استفاده کنید:", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "اتاق با آدرس %(alias)s حذف شده و نام %(name)s از فهرست اتاق‌ها نیز پاک شود؟", + "We recommend you change your password and Security Key in Settings immediately": "ما به شما توصیه می‌کنیم بلافاصله گذرواژه و کلید امنیتی خود را در تنظیمات تغییر دهید", + "The internet connection either session is using": "اتصال اینترنت و یا نشست در حال استفاده است", + "%(brand)s does not know how to join a room on this network": "%(brand)s نمی‌تواند به اتاقی در این شبکه بپیوندد", + "Data on this screen is shared with %(widgetDomain)s": "داده‌های این صفحه با %(widgetDomain)s به اشتراک گذاشته می‌شود", + "Your homeserver doesn't seem to support this feature.": "به نظر نمی‌رسد که سرور شما از این قابلیت پشتیبانی کند.", + "Unable to look up room ID from server": "جستجوی شناسه اتاق از سرور انجام نشد", + "Not currently indexing messages for any room.": "در حال حاضر ایندکس پیام ها برای هیچ اتاقی انجام نمی‌شود.", + "Session ID": "شناسه‌ی نشست", + "Confirm this user's session by comparing the following with their User Settings:": "این نشست کاربر را از طریق مقایسه‌ی این با تنظیمات کاربری تائيد کنید:", + "Confirm by comparing the following with the User Settings in your other session:": "از طریق مقایسه‌ی این با تنظیمات کاربری در نشست‌های دیگرتان، تائيد کنید:", + "Are you sure you want to sign out?": "آیا مطمئن هستید که می خواهید از برنامه خارج شوید؟", + "You'll lose access to your encrypted messages": "دسترسی به پیام‌های رمزشده‌ی خود را از دست خواهید داد", + "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "پیام‌های رمزشده با رمزنگاری سرتاسر ایمن می‌شوند. فقط شما و طرف گیرنده(ها) کلیدهای خواندن این پیام ها را در اختیار دارید.", + "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "هم‌اکنون %(brand)s از طریق بارگیری و نمایش اطلاعات کاربران تنها در زمان‌هایی که نیاز است، حدود ۳ تا ۵ مرتبه حافظه‌ی کمتری استفاده می‌کند. لطفا تا همگام‌سازی با سرور منتظر بمانید!", + "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "اگر نسخه دیگری از %(brand)s هنوز در تب‌های دیگر باز است، لطفاً آن را ببندید زیرا استفاده از %(brand)s با قابلیت بارگیری تکه‌تکه‌ی فعال روی یکی و غیرفعال روی دیگری، باعث ایجاد مشکل می شود.", + "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "شما از %(brand)s بر روی %(host)s با قابلیت بارگیری اعضا به شکل تکه‌تکه استفاده می‌کنید. در این نسخه قابلیت بارگیری تکه‌تکه غیرفعال است. از آن‌جایی که حافظه‌ی کش مورد استفاده برای این دو پیکربندی با هم سازگار نیست، %(brand)s نیاز به همگام‌سازی مجدد حساب کاربری شما دارد.", + "%(brand)s encountered an error during upload of:": "%(brand)s در حین بارگذاری این دچار مشکل شد:", + "Transfer": "منتقل کردن", + "Invited people will be able to read old messages.": "افراد دعوت‌شده خواهند توانست پیام‌های قدیمی را بخوانند.", + "Invite someone using their name, username (like ) or share this room.": "با استفاده از نام یا نام کاربری (مانند ) از افراد دعوت کرده و یا این اتاق را به اشتراک بگذارید.", + "Invite someone using their name, email address, username (like ) or share this room.": "با استفاده از نام، آدرس ایمیل، نام کاربری (مانند ) از فردی دعوت کرده و یا این اتاق را به اشتراک بگذارید.", + "Invite someone using their name, email address, username (like ) or share this space.": "با استفاده از نام ، آدرس ایمیل ، نام کاربری (مانند ) کسی را دعوت کرده یا این فضای کاری را به اشتراک بگذارید.", + "Invite someone using their name, username (like ) or share this space.": "با استفاده از نام یا نام کاربری (مانند ) از افراد دعوت کرده و یا این فضای کاری را به اشتراک بگذارید.", + "Go": "برو", + "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "این کار آنها را به %(communityName)s دعوت نمی‌کند. برای دعوت افراد به %(communityName)s،اینجا کلیک کنید", + "Start a conversation with someone using their name or username (like ).": "با استفاده از نام یا نام کاربری (مانند )، گفتگوی جدیدی را با دیگران شروع کنید.", + "Start a conversation with someone using their name, email address or username (like ).": "با استفاده از نام، آدرس ایمیل و یا نام کاربری (مانند )، یک گفتگوی جدید را شروع کنید.", + "May include members not in %(communityName)s": "ممکن شامل اعضایی که در %(communityName)s نیستند نیز شود" } diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 5452176cd9..23140846b3 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -2950,5 +2950,58 @@ "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Uusi kirjautuminen tilillesi: %(name)s (%(deviceID)s) osoitteesta %(ip)s", "This homeserver has been blocked by its administrator.": "Tämä kotipalvelin on ylläpitäjänsä estämä.", "You're already in a call with this person.": "Olet jo puhelussa tämän henkilön kanssa.", - "Already in call": "Olet jo puhelussa" + "Already in call": "Olet jo puhelussa", + "Please choose a strong password": "Valitse vahva salasana", + "You can add more later too, including already existing ones.": "Voit lisätä niitä myöhemmin, mukaan lukien olemassa olevia.", + "Let's create a room for each of them.": "Tehdään huone jokaiselle.", + "What do you want to organise?": "Mitä haluat järjestää?", + "Random": "Satunnainen", + "Search names and descriptions": "Etsi nimistä ja kuvauksista", + "Failed to remove some rooms. Try again later": "Joitakin huoneita ei voitu poistaa. Yritä myöhemmin uudelleen.", + "Select a room below first": "Valitse ensin huone alta", + "You can select all or individual messages to retry or delete": "Voit valita kaikki tai yksittäisiä viestejä yritettäväksi uudelleen tai poistettavaksi", + "Sending": "Lähetetään", + "Retry all": "Yritä kaikkia uudelleen", + "Delete all": "Poista kaikki", + "Some of your messages have not been sent": "Osaa viesteistäsi ei ole lähetetty", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Olet ainoa henkilö täällä. Jos lähdet, kukaan ei voi liittyä tulevaisuudessa, et myöskään sinä.", + "Beta": "Beeta", + "Tap for more info": "Lisää tietoa napauttamalla", + "The server is not configured to indicate what the problem is (CORS).": "Palvelinta ei ole säädetty ilmoittamaan, mikä ongelma on kyseessä (CORS).", + "Invited people will be able to read old messages.": "Kutsutut ihmiset voivat lukea vanhoja viestejä.", + "We couldn't create your DM.": "Yksityisviestiä ei voitu luoda.", + "Thank you for your feedback, we really appreciate it.": "Kiitos palautteesta, arvostamme sitä.", + "Beta feedback": "Palautetta beetaversiosta", + "Want to add a new room instead?": "Haluatko kuitenkin lisätä uuden huoneen?", + "Add existing rooms": "Lisää olemassa olevia huoneita", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Lisätään huonetta...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Lisätään huoneita... (%(progress)s out of %(count)s)", + "Not all selected were added": "Kaikkia valittuja ei lisätty", + "You are not allowed to view this server's rooms list": "Sinulla ei ole oikeuksia nähdä tämän palvelimen huoneluetteloa", + "View message": "Näytä viesti", + "%(count)s people you know have already joined|one": "%(count)s tuntemasi henkilö on jo liittynyt", + "%(count)s people you know have already joined|other": "%(count)s tuntemaasi ihmistä on jo liittynyt", + "View all %(count)s members|one": "Näytä yksi jäsen", + "View all %(count)s members|other": "Näytä kaikki %(count)s jäsentä", + "Add reaction": "Lisää reaktio", + "Error processing voice message": "Virhe ääniviestin käsittelyssä", + "Delete recording": "Poista äänitys", + "Stop the recording": "Lopeta äänitys", + "Record a voice message": "Äänitä viesti", + "We were unable to access your microphone. Please check your browser settings and try again.": "Mikrofoniasi ei voitu käyttää. Tarkista selaimesi asetukset ja yritä uudelleen.", + "We didn't find a microphone on your device. Please check your settings and try again.": "Laitteestasi ei löytynyt mikrofonia. Tarkista asetuksesi ja yritä uudelleen.", + "No microphone found": "Mikrofonia ei löytynyt", + "Unable to access your microphone": "Mikrofonia ei voi käyttää", + "Quick actions": "Pikatoiminnot", + "%(seconds)ss left": "%(seconds)s s jäljellä", + "Failed to send": "Lähettäminen epäonnistui", + "You have no ignored users.": "Et ole sivuuttanut käyttäjiä.", + "Warn before quitting": "Varoita ennen lopettamista", + "Manage & explore rooms": "Hallitse ja selaa huoneita", + "Connecting": "Yhdistetään", + "unknown person": "tuntematon henkilö", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Salli vertaisyhteydet 1:1-puheluille (jos otat tämän käyttöön, toinen osapuoli saattaa nähdä IP-osoitteesi)", + "Send and receive voice messages": "Lähetä ja vastaanota ääniviestejä", + "Show options to enable 'Do not disturb' mode": "Näytä asetukset Älä häiritse -tilan ottamiseksi käyttöön", + "%(deviceId)s from %(ip)s": "%(deviceId)s osoitteesta %(ip)s" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 984dce8595..5a8208f50b 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -936,7 +936,7 @@ "Failed to load group members": "Échec du chargement des membres du groupe", "Failed to invite users to the room:": "Échec de l’invitation d'utilisateurs dans le salon :", "There was an error joining the room": "Une erreur est survenue en rejoignant le salon", - "You do not have permission to invite people to this room.": "Vous n’avez pas la permission d’envoyer des invitations dans ce salon.", + "You do not have permission to invite people to this room.": "Vous n’avez pas la permission d’inviter des personnes dans ce salon.", "User %(user_id)s does not exist": "L’utilisateur %(user_id)s n’existe pas", "Unknown server error": "Erreur de serveur inconnue", "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Afficher un rappel pour activer la récupération de messages sécurisée dans les salons chiffrés", @@ -3024,7 +3024,7 @@ "Use Command + F to search": "Utilisez Commande + F pour rechercher", "Show line numbers in code blocks": "Afficher les numéros de ligne dans les blocs de code", "Expand code blocks by default": "Développer les blocs de code par défaut", - "Show stickers button": "Afficher le bouton autocollants", + "Show stickers button": "Afficher le bouton des autocollants", "Use app": "Utiliser l’application", "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web est expérimental sur téléphone. Pour une meilleure expérience et bénéficier des dernières fonctionnalités, utilisez notre application native gratuite.", "Use app for a better experience": "Utilisez une application pour une meilleure expérience", @@ -3175,8 +3175,8 @@ "Delete": "Supprimer", "Jump to the bottom of the timeline when you send a message": "Sauter en bas du fil de discussion lorsque vous envoyez un message", "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Prototype d’espaces. Incompatible avec les communautés, les communautés v2 et les étiquettes personnalisées. Nécessite un serveur d’accueil compatible pour certaines fonctionnalités.", - "This homeserver has been blocked by it's administrator.": "Ce serveur d’accueil a été banni par ses administrateurs.", - "This homeserver has been blocked by its administrator.": "Ce serveur d’accueil a été banni par ses administrateurs.", + "This homeserver has been blocked by it's administrator.": "Ce serveur d’accueil a été bloqué par son administrateur.", + "This homeserver has been blocked by its administrator.": "Ce serveur d’accueil a été bloqué par son administrateur.", "You're already in a call with this person.": "Vous êtes déjà en cours d’appel avec cette personne.", "Already in call": "Déjà en cours d’appel", "Space selection": "Sélection d’un espace", @@ -3285,5 +3285,72 @@ "Including %(commaSeparatedMembers)s": "Dont %(commaSeparatedMembers)s", "View all %(count)s members|one": "Afficher le membre", "View all %(count)s members|other": "Afficher les %(count)s membres", - "Failed to send": "Échec de l’envoi" + "Failed to send": "Échec de l’envoi", + "Play": "Lecture", + "Pause": "Pause", + "Enter your Security Phrase a second time to confirm it.": "Saisissez à nouveau votre phrase secrète pour la confirmer.", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Choisissez des salons ou conversations à ajouter. C’est un espace rien que pour vous, personne n’en sera informé. Vous pourrez en ajouter plus tard.", + "What do you want to organise?": "Que voulez-vous organiser ?", + "Filter all spaces": "Filtrer tous les espaces", + "Delete recording": "Supprimer l’enregistrement", + "Stop the recording": "Arrêter l’enregistrement", + "%(count)s results in all spaces|one": "%(count)s résultat dans tous les espaces", + "%(count)s results in all spaces|other": "%(count)s résultats dans tous les espaces", + "You have no ignored users.": "Vous n’avez ignoré personne.", + "Your access token gives full access to your account. Do not share it with anyone.": "Votre jeton d’accès donne un accès intégral à votre compte. Ne le partagez avec personne.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Ceci est une fonctionnalité expérimentale. Pour l’instant, les nouveaux utilisateurs recevant une invitation devront l’ouvrir sur pour poursuivre.", + "To join %(spaceName)s, turn on the Spaces beta": "Pour rejoindre %(spaceName)s, activez les espaces en bêta", + "To view %(spaceName)s, turn on the Spaces beta": "Pour visualiser %(spaceName)s, activez les espaces en bêta", + "Select a room below first": "Sélectionnez un salon ci-dessous d’abord", + "Communities are changing to Spaces": "Les communautés deviennent des espaces", + "Join the beta": "Rejoindre la bêta", + "Leave the beta": "Quitter la bêta", + "Beta": "Bêta", + "Tap for more info": "Appuyez pour plus d’information", + "Spaces is a beta feature": "Les espaces sont une fonctionnalité en bêta", + "Want to add a new room instead?": "Voulez-vous plutôt ajouter un nouveau salon ?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Ajout du salon…", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Ajout des salons… (%(progress)s sur %(count)s)", + "Not all selected were added": "Toute la sélection n’a pas été ajoutée", + "You can add existing spaces to a space.": "Vous pouvez ajouter des espaces existants à un espace.", + "Feeling experimental?": "L’esprit aventurier ?", + "You are not allowed to view this server's rooms list": "Vous n’avez pas l’autorisation d’accéder à la liste des salons de ce serveur", + "Error processing voice message": "Erreur lors du traitement du message vocal", + "We didn't find a microphone on your device. Please check your settings and try again.": "Nous n’avons pas détecté de microphone sur votre appareil. Merci de vérifier vos paramètres et de réessayer.", + "No microphone found": "Aucun microphone détecté", + "We were unable to access your microphone. Please check your browser settings and try again.": "Nous n’avons pas pu accéder à votre microphone. Merci de vérifier les paramètres de votre navigateur et de réessayer.", + "Unable to access your microphone": "Impossible d’accéder à votre microphone", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "L’esprit aventurier ? Les fonctionnalités expérimentales vous permettent de tester les nouveautés et aider à les polir avant leur lancement. En apprendre plus.", + "Access Token": "Jeton d’accès", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Les espaces sont un nouveau moyen de grouper les salons et les personnes. Une invitation est nécessaire pour rejoindre un espace existant.", + "Please enter a name for the space": "Veuillez renseigner un nom pour l’espace", + "Connecting": "Connexion", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Autoriser le pair-à-pair (p2p) pour les appels individuels (si activé, votre correspondant pourra voir votre adresse IP)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Bêta disponible pour l’application web, de bureau et Android. Certains fonctionnalités pourraient ne pas être disponibles sur votre serveur d’accueil.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Vous pouvez quitter la bêta n’importe quand à partir des paramètres, ou en appuyant sur le badge bêta comme celui ci-dessus.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s va être redémarré avec les espaces activés. Les communautés et les étiquettes personnalisées seront cachés.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Bêta disponible pour l’application web, de bureau et Android. Merci d’essayer la bêta.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s va être redémarré avec les espaces désactivés. Les communautés et les étiquettes personnalisées seront de nouveau visibles.", + "Spaces are a new way to group rooms and people.": "Les espaces sont un nouveau moyen de regrouper les salons et les personnes.", + "Message search initialisation failed": "Échec de l’initialisation de la recherche de message", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Vos commentaires aideront à améliorer les espaces. N’hésitez pas à entrer dans les détails.", + "%(featureName)s beta feedback": "Commentaires sur la bêta de %(featureName)s", + "Thank you for your feedback, we really appreciate it.": "Merci pour vos commentaires, nous en sommes vraiment reconnaissants.", + "Beta feedback": "Commentaires sur la bêta", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Si vous quittez, %(brand)s sera rechargé avec les espaces désactivés. Les communautés et les étiquettes personnalisées seront à nouveau visibles.", + "Spaces are a beta feature.": "Les espaces sont une fonctionnalité en bêta.", + "Search names and descriptions": "Rechercher par nom et description", + "You may contact me if you have any follow up questions": "Vous pouvez me contacter si vous avez des questions par la suite", + "To leave the beta, visit your settings.": "Pour quitter la bêta, consultez les paramètres.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Votre plateforme et nom d’utilisateur seront consignés pour nous aider à tirer le maximum de vos retours.", + "Add reaction": "Ajouter une réaction", + "Send and receive voice messages": "Envoyer et recevoir des messages vocaux", + "See when people join, leave, or are invited to this room": "Voir quand une personne rejoint, quitte ou est invitée sur ce salon", + "Kick, ban, or invite people to this room, and make you leave": "Exclure, bannir ou inviter une personne dans ce salon et vous permettre de partir", + "Space Autocomplete": "Autocomplétion d’espace", + "Go to my space": "Aller à mon espace", + "sends space invaders": "Envoie les Space Invaders", + "Sends the given message with a space themed effect": "Envoyer le message avec un effet lié au thème de l’espace", + "See when people join, leave, or are invited to your active room": "Afficher quand des personnes rejoignent, partent, ou sont invités dans votre salon actif", + "Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir" } diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 7b816e94cd..12a2dcd8c3 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3308,5 +3308,72 @@ "Including %(commaSeparatedMembers)s": "Incluíndo a %(commaSeparatedMembers)s", "View all %(count)s members|one": "Ver 1 membro", "View all %(count)s members|other": "Ver tódolos %(count)s membros", - "Failed to send": "Fallou o envío" + "Failed to send": "Fallou o envío", + "Enter your Security Phrase a second time to confirm it.": "Escribe a túa Frase de Seguridade por segunda vez para confirmala.", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Elixe salas ou conversas para engadilas. Este é un espazo para ti, ninguén será notificado. Podes engadir máis posteriormente.", + "What do you want to organise?": "Que queres organizar?", + "Filter all spaces": "Filtrar os espazos", + "Delete recording": "Eliminar a gravación", + "Stop the recording": "Deter a gravación", + "%(count)s results in all spaces|one": "%(count)s resultado en tódolos espazos", + "%(count)s results in all spaces|other": "%(count)s resultados en tódolos espazos", + "You have no ignored users.": "Non tes usuarias ignoradas.", + "Play": "Reproducir", + "Pause": "Deter", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Esta é unha característica experimental. Por agora as novas usuarias convidadas deberán abrir o convite en para poder unirse.", + "To join %(spaceName)s, turn on the Spaces beta": "Para unirte a %(spaceName)s, activa a beta de Espazos", + "To view %(spaceName)s, turn on the Spaces beta": "Para ver %(spaceName)s, cambia á beta de Espazos", + "Select a room below first": "Primeiro elixe embaixo unha sala", + "Communities are changing to Spaces": "Comunidades cambia a Espazos", + "Join the beta": "Unirse á beta", + "Leave the beta": "Saír da beta", + "Beta": "Beta", + "Tap for more info": "Toca para ter máis información", + "Spaces is a beta feature": "Espazos é unha característica en beta", + "Want to add a new room instead?": "Queres engadir unha nova sala?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Engadindo sala...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Engadindo salas... (%(progress)s de %(count)s)", + "Not all selected were added": "Non se engadiron tódolos seleccionados", + "You can add existing spaces to a space.": "Podes engadir espazos existentes a un espazo.", + "Feeling experimental?": "Sínteste aventureira?", + "You are not allowed to view this server's rooms list": "Non tes permiso para ver a lista de salas deste servidor", + "Error processing voice message": "Erro ao procesar a mensaxe de voz", + "We didn't find a microphone on your device. Please check your settings and try again.": "Non atopamos ningún micrófono no teu dispositivo. Comproba os axustes e proba outra vez.", + "No microphone found": "Non atopamos ningún micrófono", + "We were unable to access your microphone. Please check your browser settings and try again.": "Non puidemos acceder ao teu micrófono. Comproba os axustes do navegador e proba outra vez.", + "Unable to access your microphone": "Non se puido acceder ao micrófono", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Gañas de experimentar? Labs é o mellor xeito para un acceso temperá e probar novas funcións e axudar a melloralas antes de ser publicadas. Coñece máis.", + "Your access token gives full access to your account. Do not share it with anyone.": "O teu token de acceso da acceso completo á túa conta. Non o compartas con ninguén.", + "Access Token": "Token de acceso", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Espazos é un novo xeito de agrupar salas e persoas. Precisas un convite para unirte a un espazo existente.", + "Please enter a name for the space": "Escribe un nome para o espazo", + "Connecting": "Conectando", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Permitir Peer-to-Peer en chamadas 1:1 (se activas isto a outra parte podería coñecer o teu enderezo IP)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta dispoñible para web, escritorio e Android. Algunhas características poderían non estar dispoñibles no teu servidor de inicio.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Podes saír da beta desde os axustes cando queiras ou tocando na insignia beta, como a superior.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s cargará con Espazos activado. Comunidades e etiquetas personais estarán agochadas.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta dispoñible para web, escritorio e Android. Grazas por probar a beta.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s volverá a cargar con Espazos desactivado. Comunidade e etiquetas personalizadas estarán visibles de volta.", + "Spaces are a new way to group rooms and people.": "Espazos é un novo xeito de agrupar salas e persoas.", + "Spaces are a beta feature.": "Espazos é unha ferramenta en beta.", + "Search names and descriptions": "Buscar nome e descricións", + "You may contact me if you have any follow up questions": "Podes contactar conmigo se tes algunha outra suxestión", + "To leave the beta, visit your settings.": "Para saír da beta, vai aos axustes.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "A túa plataforma e nome de usuaria serán notificados para axudarnos a utilizar a túa opinión do mellor xeito posible.", + "%(featureName)s beta feedback": "Opinión acerca de %(featureName)s beta", + "Thank you for your feedback, we really appreciate it.": "Grazas pola túa opinión, realmente apreciámola.", + "Beta feedback": "Opinión sobre a beta", + "Add reaction": "Engadir reacción", + "Send and receive voice messages": "Enviar e recibir mensaxes de voz", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "A túa opinión axudaranos a mellorar os espazos. Canto máis detallada sexa moito mellor para nós.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se saes, %(brand)s volverá a cargar con Espazos desactivados. Comunidades e etiquetas personais serán visibles outra vez.", + "Message search initialisation failed": "Fallou a inicialización da busca de mensaxes", + "Space Autocomplete": "Autocompletado do espazo", + "Go to my space": "Ir ao meu espazo", + "sends space invaders": "enviar invasores espaciais", + "Sends the given message with a space themed effect": "Envía a mensaxe cun efecto de decorado espacial", + "See when people join, leave, or are invited to your active room": "Mira cando alguén se une, sae ou é convidada á túa sala activa", + "Kick, ban, or invite people to your active room, and make you leave": "Expulsa, veta ou convida a persoas á túa sala activa, e fai que saias", + "See when people join, leave, or are invited to this room": "Mira cando se une alguén, sae ou é convidada a esta sala", + "Kick, ban, or invite people to this room, and make you leave": "Expulsa, veta, ou convida persoas a esta sala, e fai que saias" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index e6e5575674..a6e9992866 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3303,5 +3303,72 @@ "Including %(commaSeparatedMembers)s": "Beleértve: %(commaSeparatedMembers)s", "View all %(count)s members|one": "1 résztvevő megmutatása", "View all %(count)s members|other": "Az összes %(count)s résztvevő megmutatása", - "Failed to send": "Küldés sikertelen" + "Failed to send": "Küldés sikertelen", + "Enter your Security Phrase a second time to confirm it.": "A megerősítéshez adja meg a biztonsági jelmondatot még egyszer.", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Válassz szobákat vagy beszélgetéseket amit hozzáadhat. Ez csak az ön tere, senki nem lesz értesítve. Továbbiakat később is hozzáadhat.", + "What do you want to organise?": "Mit szeretne megszervezni?", + "Filter all spaces": "Minden tér szűrése", + "Delete recording": "Felvétel törlése", + "Stop the recording": "Felvétel megállítása", + "%(count)s results in all spaces|one": "%(count)s találat van az összes térben", + "%(count)s results in all spaces|other": "%(count)s találat a terekben", + "You have no ignored users.": "Nincs figyelmen kívül hagyott felhasználó.", + "Play": "Lejátszás", + "Pause": "Szünet", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Ez egy kísérleti funkció Egyenlőre az a felhasználó aki meghívót kap a meghívóban lévő linkre kattintva tud csatlakozni.", + "To join %(spaceName)s, turn on the Spaces beta": "A csatlakozáshoz ide: %(spaceName)s először kapcsolja be a béta Tereket", + "To view %(spaceName)s, turn on the Spaces beta": "A %(spaceName)s megjelenítéséhez először kapcsolja be a béta Tereket", + "Select a room below first": "Először válasszon ki szobát alulról", + "Communities are changing to Spaces": "A közösségek Terek lesznek", + "Join the beta": "Csatlakozás béta lehetőségekhez", + "Leave the beta": "Béta kikapcsolása", + "Beta": "Béta", + "Tap for more info": "Koppints további információért", + "Spaces is a beta feature": "A terek béta állapotban van", + "Want to add a new room instead?": "Inkább új szobát adna hozzá?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Szobák hozzáadása…", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Szobák hozzáadása… (%(progress)s ennyiből: %(count)s)", + "Not all selected were added": "Nem az összes kijelölt lett hozzáadva", + "You can add existing spaces to a space.": "Létező tereket adhat a térhez.", + "Feeling experimental?": "Kísérletezni szeretne?", + "You are not allowed to view this server's rooms list": "Nincs joga ennek a szervernek a szobalistáját megnézni", + "Error processing voice message": "Hiba a hangüzenet feldolgozásánál", + "We didn't find a microphone on your device. Please check your settings and try again.": "Nem található mikrofon. Ellenőrizze a beállításokat és próbálja újra.", + "No microphone found": "Nem található mikrofon", + "We were unable to access your microphone. Please check your browser settings and try again.": "Nem lehet a mikrofont használni. Ellenőrizze a böngésző beállításait és próbálja újra.", + "Unable to access your microphone": "A mikrofont nem lehet használni", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Kedve van kísérletezni? Labs az a hely ahol először hozzá lehet jutni az új dolgokhoz, kipróbálni új lehetőségeket és segíteni a fejlődésüket mielőtt mindenkihez eljut. Tudj meg többet.", + "Your access token gives full access to your account. Do not share it with anyone.": "A hozzáférési kulcs teljes elérést biztosít a fiókhoz. Soha ne ossza meg mással.", + "Access Token": "Elérési kulcs", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "A terek egy új lehetőség a szobák és emberek csoportosításához. Létező térhez meghívóval lehet csatlakozni.", + "Please enter a name for the space": "Kérem adjon meg egy nevet a térhez", + "Connecting": "Kapcsolás", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Közvetlen hívás engedélyezése két fél között (ha ezt engedélyezi a másik fél láthatja az ön IP címét)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Béta verzió elérhető webre, asztali kliensre és Androidra. Bizonyos funkciók lehet, hogy nem elérhetők a matrix szerverén.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Bármikor elhagyhatja a béta változatot a beállításokban vagy a béta kitűzőre koppintva, mint alább.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s a Terekkel lesz újra betöltve. A közösségek és egyedi címkék rejtve maradnak.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Béta verzió elérhető webre, asztali kliensre és Androidra. Köszönjük, hogy kipróbálja.", + "Spaces are a new way to group rooms and people.": "Szobák és emberek csoportosításának új lehetősége a Terek használata.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s a Terek nélkül lesz újra betöltve. A közösségek és egyedi címkék újra megjelennek.", + "To leave the beta, visit your settings.": "A beállításokban tudja elhagyni a bétát.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "A platform és a felhasználói neve felhasználásra kerül ami segít nekünk a visszajelzést minél jobban felhasználni.", + "%(featureName)s beta feedback": "%(featureName)s béta visszajelzés", + "Thank you for your feedback, we really appreciate it.": "Köszönjük a visszajelzését, ezt nagyra értékeljük.", + "Beta feedback": "Béta visszajelzés", + "Add reaction": "Reakció hozzáadása", + "Send and receive voice messages": "Hangüzenet küldése, fogadása", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "A visszajelzése segítség a terek javításához. Minél részletesebb annál jobb.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Távozás után %(brand)s Terek nélkül lesz újra betöltve. A közösségek és egyedi címkék újra megjelennek.", + "Message search initialisation failed": "Üzenet keresés beállítása sikertelen", + "Space Autocomplete": "Tér automatikus kiegészítése", + "Go to my space": "Irány a teréhez", + "Spaces are a beta feature.": "A terek béta állapotban van.", + "Search names and descriptions": "Nevek és leírások keresése", + "You may contact me if you have any follow up questions": "Ha további kérdés merülne fel, kapcsolatba léphetnek velem", + "sends space invaders": "space invaders küldése", + "Sends the given message with a space themed effect": "Üzenet küldése világűrös effekttel", + "See when people join, leave, or are invited to your active room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése az aktív szobájában", + "Kick, ban, or invite people to your active room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket az aktív szobába és, hogy ön elhagyja a szobát", + "See when people join, leave, or are invited to this room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése ebben a szobában", + "Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát" } diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index ddb3bbc66d..35f5342b30 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -136,7 +136,7 @@ "Attachment": "Viðhengi", "Hangup": "Leggja á", "Voice call": "Raddsamtal", - "Video call": "_Myndsímtal", + "Video call": "Myndsímtal", "Upload file": "Hlaða inn skrá", "Send an encrypted message…": "Senda dulrituð skilaboð…", "You do not have permission to post to this room": "Þú hefur ekki heimild til að senda skilaboð á þessa spjallrás", @@ -198,7 +198,7 @@ "Today": "Í dag", "Yesterday": "Í gær", "Error decrypting attachment": "Villa við afkóðun viðhengis", - "Copied!": "Afritað", + "Copied!": "Afritað!", "Custom Server Options": "Sérsniðnir valkostir vefþjóns", "Dismiss": "Hunsa", "Please check your email to continue registration.": "Skoðaðu tölvupóstinn þinn til að geta haldið áfram með skráningu.", @@ -460,5 +460,265 @@ "Create Account": "Stofna Reikning", "Please install Chrome, Firefox, or Safari for the best experience.": "vinsamlegast setja upp Chrome, Firefox, eða Safari fyrir besta reynsluna.", "Explore rooms": "Kanna herbergi", - "Sign In": "Skrá inn" + "Sign In": "Skrá inn", + "The user's homeserver does not support the version of the room.": "Heimaþjónn notandans styður ekki útgáfu herbergis.", + "The user must be unbanned before they can be invited.": "Notandinn þarf að vera afbannaður áður en að hægt er að bjóða þeim.", + "User %(user_id)s may or may not exist": "Notandi %(user_id)s gæti verið til", + "User %(user_id)s does not exist": "Notandi %(user_id)s er ekki til", + "User %(userId)s is already in the room": "Notandi %(userId)s er nú þegar í herberginu", + "You do not have permission to invite people to this room.": "Þú hefur ekki heimild til að bjóða fólk í þessa spjallrás.", + "Leave Room": "Fara af Spjallrás", + "Add room": "Bæta við herbergi", + "Use a more compact ‘Modern’ layout": "Nota þéttara ‘nútímalegt’ skipulag", + "Switch to dark mode": "Skiptu yfir í dökkstillingu", + "Switch to light mode": "Skiptu yfir í ljósstillingu", + "Modify widgets": "Breyta viðmótshluta", + "Room Info": "Herbergis upplýsingar", + "Room information": "Upplýsingar um herbergi", + "Room options": "Herbergisvalkostir", + "Invite People": "Bjóða Fólki", + "Invite people": "Bjóða fólki", + "%(count)s people|other": "%(count)s manns", + "%(count)s people|one": "%(count)s manneskja", + "People": "Fólk", + "Finland": "Finnland", + "Norway": "Noreg", + "Denmark": "Danmörk", + "Iceland": "Ísland", + "Mentions & Keywords": "Nefnir og stikkorð", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Ef þú hættir við núna geturðu tapað dulkóðuðum skilaboðum og gögnum ef þú missir aðgang að innskráningum þínum.", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Heimaþjónastjórnandi þinn hefur lokað á sjálfkrafa dulkóðun í einkaherbergjum og beinskilaboðum.", + "You can’t disable this later. Bridges & most bots won’t work yet.": "Þú getur ekki gert þetta óvirkt síðar. Brýr og flest vélmenni virka ekki ennþá.", + "Travel & Places": "Ferðalög og staðir", + "Food & Drink": "Mat og drykkur", + "Animals & Nature": "Dýr og náttúra", + "Smileys & People": "Broskarlar og fólk", + "Voice & Video": "Rödd og myndband", + "Roles & Permissions": "Hlutverk og heimildir", + "Help & About": "Hjálp og um", + "Reject & Ignore user": "Hafna og hunsa notanda", + "Security & privacy": "Öryggi og einkalíf", + "Security & Privacy": "Öryggi & Einkalíf", + "Feedback sent": "Endurgjöf sent", + "Send feedback": "Senda endurgjöf", + "Feedback": "Endurgjöf", + "%(featureName)s beta feedback": "%(featureName)s beta endurgjöf", + "Thank you for your feedback, we really appreciate it.": "Þakka þér fyrir athugasemdir þínar.", + "Beta feedback": "Beta endurgjöf", + "All settings": "Allar stillingar", + "Notification settings": "Tilkynningarstillingar", + "Change notification settings": "Breytta tilkynningastillingum", + "You can't send any messages until you review and agree to our terms and conditions.": "Þú getur ekki sent nein skilaboð fyrr en þú hefur farið yfir og samþykkir skilmála okkar.", + "Send a Direct Message": "Senda beinskilaboð", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Tilkynning um þessi skilaboð mun senda einstakt 'atburðarauðkenni' til stjórnanda heimaþjónns. Ef skilaboð í þessu herbergi eru dulkóðuð getur stjórnandi heimaþjónns ekki lesið skilaboðatextann eða skoðað skrár eða myndir.", + "Send a message…": "Senda skilaboð…", + "Send message": "Senda skilaboð", + "Sending your message...": "Er að senda skilaboð þitt...", + "Send as message": "Senda sem skilaboð", + "You can use /help to list available commands. Did you mean to send this as a message?": "Þú getur notað /help til að lista tilteknar skipanir. Ætlaðir þú að senda þetta sem skilaboð?", + "Send messages": "Senda skilaboð", + "Sends the given message with snowfall": "Sendir skilaboðið með snjókomu", + "Sends the given message with fireworks": "Sendir skilaboðið með flugeldum", + "Sends the given message with confetti": "Sendir skilaboðið með skrauti", + "Never send encrypted messages to unverified sessions in this room from this session": "Aldrei senda dulrituð skilaboð af þessu tæki til ósannvottaðra tækja í þessu herbergi", + "Never send encrypted messages to unverified sessions from this session": "Aldrei senda dulrituð skilaboð af þessu tæki til ósannvottaðra tækja", + "Use Ctrl + Enter to send a message": "Notaðu Ctrl + Enter til að senda skilaboð", + "Use Command + Enter to send a message": "Notaðu Command + Enter til að senda skilaboð", + "Jump to the bottom of the timeline when you send a message": "Hoppaðu neðst á tímalínunni þegar þú sendir skilaboð", + "Send and receive voice messages": "Senda og taka á móti talskilaboðum", + "%(senderName)s: %(message)s": "%(senderName)s: %(message)s", + "Send %(msgtype)s messages as you in your active room": "Senda %(msgtype)s skilaboð sem þú í virka herbergi þínu", + "Send %(msgtype)s messages as you in this room": "Senda %(msgtype)s skilaboð sem þú í þessu herbergi", + "Send text messages as you in your active room": "Senda texta skilaboð sem þú í virku herbergi þínu", + "Send text messages as you in this room": "Senda texta skilaboð sem þú í þessu herbergi", + "Send messages as you in your active room": "Senda skilaboð sem þú í virku herbergi þínu", + "Send messages as you in this room": "Senda skilaboð sem þú í þessu herbergi", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s breytti föstum skilaboðum fyrir herbergið.", + "Sends a message to the given user": "Sendir skilaboð til viðkomandi notanda", + "Sends the given message coloured as a rainbow": "Sendir gefið skilaboð litað sem regnbogi", + "Sends a message as html, without interpreting it as markdown": "Sendir skilaboð sem html, án þess að túlka það sem markdown", + "Sends a message as plain text, without interpreting it as markdown": "Sendir skilaboð sem óbreyttur texti án þess að túlka það sem markdown", + "Sends the given message as a spoiler": "Sendir skilaboðið sem spoiler", + "No need for symbols, digits, or uppercase letters": "Engin þörf á táknum, tölustöfum, eða hástöfum", + "Use a few words, avoid common phrases": "Notaðu nokkur orð. Forðastu algengar setningar", + "Unknown server error": "Óþekkt villa á þjóni", + "Message deleted by %(name)s": "Skilaboð eytt af %(name)s", + "Message deleted": "Skilaboð eytt", + "Room list": "Herbergislisti", + "Subscribed lists": "Skráðir listar", + "eg: @bot:* or example.org": "t.d.: @vélmenni:* eða dæmi.is", + "Personal ban list": "Persónulegur bann listi", + "⚠ These settings are meant for advanced users.": "⚠ Þessar stillingar eru ætlaðar fyrir háþróaða notendur.", + "Ignored users": "Hunsaðir notendur", + "You are currently subscribed to:": "Þú ert skráður til:", + "View rules": "Skoða reglur", + "You are not subscribed to any lists": "Þú ert ekki skráður fyrir neina lista", + "You are currently ignoring:": "Þú ert að hunsa:", + "You have not ignored anyone.": "Þú hefur ekki hunsað nein.", + "User rules": "Reglur notanda", + "Server rules": "Reglur netþjóns", + "Please try again or view your console for hints.": "Vinsamlegast reyndu aftur eða skoðaðu framkvæmdaraðilaatvikuskrá þína fyrir vísbendingar.", + "Error unsubscribing from list": "Galli við að afskrá frá lista", + "Error removing ignored user/server": "Villa við að fjarlægja hunsaða notanda/netþjón", + "Use the Desktop app to search encrypted messages": "Notaðu tölvuforritið til að sía dulkóðuð skilaboð", + "Use the Desktop app to see all encrypted files": "Notaðu tölvuforritið til að sjá öll dulkóðuð gögn", + "Not encrypted": "Ekki dulkóðað", + "Encrypted by a deleted session": "Dulkóðað af eyddu tæki", + "Encrypted by an unverified session": "Dulkóðað af ósannreynu tæki", + "Enable message search in encrypted rooms": "Virka skilaboðleit í dulkóðuð herbergjum", + "This room is end-to-end encrypted": "Þetta herbergi er enda-til-enda dulkóðað", + "Unencrypted": "Ódulkóðað", + "Messages in this room are end-to-end encrypted.": "Skilaboð í þessu herbergi eru enda-til-enda dulkóðuð.", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Hreinsun geymslu vafrans gæti lagað vandamálið en mun skrá þig út og valda því að dulkóðaður spjallferil sé ólæsilegur.", + "Send an encrypted reply…": "Senda dulritað svar…", + "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Þegar hún er gerð virk er ekki hægt að óvirka dulkóðun. Skilaboð í dulkóðuðu herbergi geta ekki verið séð af netþjóni en bara af þátttakendum í herberginu. Virkun dulkóðuns gæti komið í veg fyrir að vélmenni og brúr virki rétt. Lærðu meira um dulkóðun.", + "Once enabled, encryption cannot be disabled.": "Þegar kveikt er á dulkóðun er ekki hægt að slökkva á henni.", + "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.": "Í dulkóðuðum herbergjum eins og þetta er slökkt á forskoðun vefslóða sjálfgefið til að tryggja að heimaþjónn þinn (þar sem forsýningin myndast) geti ekki safnað upplýsingum um tengla sem þú sérð í þessu herbergi.", + "URL Previews": "Forskoðun Vefslóða", + "URL previews are disabled by default for participants in this room.": "Forskoðun vefslóða er ekki sjálfgefið fyrir þátttakendur í þessu herbergi.", + "URL previews are enabled by default for participants in this room.": "Forskoðun vefslóða er sjálfgefið fyrir þátttakendur í þessu herbergi.", + "You have disabled URL previews by default.": "Þú hefur óvirkt forskoðun vefslóða sjálfgefið.", + "You have enabled URL previews by default.": "Þú hefur virkt forskoðun vefslóða sjálfgefið.", + "Enable URL previews by default for participants in this room": "Virkja forskoðun vefslóða sjálfgefið fyrir þátttakendur í þessu herbergi", + "Enable URL previews for this room (only affects you)": "Virkja forskoðun vefslóða fyrir þetta herbergi (einungis fyrir þig)", + "Room settings": "Herbergisstillingar", + "Room Settings - %(roomName)s": "Herbergisstillingar - %(roomName)s", + "Pinned Messages": "Föst Skilaboð", + "No pinned messages.": "Engin föst skilaboð.", + "This is the beginning of your direct message history with .": "Þetta er upphaf beinna skilaboðasögu með .", + "Recently Direct Messaged": "Nýlega Fékk Bein Skilaboð", + "Direct Messages": "Bein skilaboð", + "Direct message": "Beint skilaboð", + "Frequently Used": "Oft notað", + "Filter all spaces": "Sía öll rými", + "Filter your rooms and spaces": "Sía rými og herbergin þín", + "Filter rooms and people": "Sía fólk og herbergi", + "Filter": "Sía", + "Your Security Key is in your Downloads folder.": "Öryggislykillinn þinn er Niðurhals möppu þinni.", + "Download logs": "Niðurhal atvikaskrá", + "Preparing to download logs": "Undirbý niðurhal atvikaskráa", + "Downloading logs": "Er að niðurhala atvikaskrá", + "Error downloading theme information.": "Villa við að niðurhala þemaupplýsingum.", + "Message downloading sleep time(ms)": "Skilaboða niðurhal svefn tími(ms)", + "How fast should messages be downloaded.": "Hve hratt ætti að hlaða niður skilaboðum.", + "Download %(text)s": "Niðurhala %(text)s", + "Share Link to User": "Deila Hlekk að Notanda", + "You have verified this user. This user has verified all of their sessions.": "Þú hefur sannreynt þennan notanda. Þessi notandi hefur sannreynt öll tæki þeirra.", + "This user has not verified all of their sessions.": "Þessi notandi hefur ekki sannreynt öll tæki þeirra.", + "%(count)s verified sessions|one": "1 sannreynt tæki", + "%(count)s verified sessions|other": "%(count)s sannreyn tæki", + "Hide verified sessions": "Fela sannreyn tæki", + "Remove recent messages": "Fjarlægja nýleg skilaboð", + "Remove recent messages by %(user)s": "Fjarlægja nýleg skilaboð af %(user)s", + "Messages in this room are not end-to-end encrypted.": "Skilaboð í þessu herbergi eru ekki enda-til-enda dulkóðuð.", + "Who would you like to add to this community?": "Hvern viltu bæta við í þetta samfélagi?", + "You cannot place a call with yourself.": "Þú getur ekki byrjað símtal með sjálfum þér.", + "You cannot place VoIP calls in this browser.": "Þú getur ekki byrjað netsímtal (VoIP) köll í þessum vafra.", + "Call Failed": "Símtal Mistókst", + "Every page you use in the app": "Sérhver síða sem þú notar í forritinu", + "Which officially provided instance you are using, if any": "Hvaða opinberlega veittan heimaþjón sem þú notar, ef einhvern", + "Whether or not you're logged in (we don't record your username)": "Hvort sem þú ert skráð(ur) inn (við skráum ekki notendanafnið þitt)", + "Add Phone Number": "Bæta Við Símanúmeri", + "Click the button below to confirm adding this phone number.": "Smelltu á hnappinn hér að neðan til að staðfesta að bæta við þessu símanúmeri.", + "Confirm adding phone number": "Staðfestu að bæta við símanúmeri", + "Add Email Address": "Bæta Við Tölvupóstfangi", + "Click the button below to confirm adding this email address.": "Smelltu á hnappinn hér að neðan til að staðfesta að bæta við þessu netfangi.", + "Confirm adding email": "Staðfestu að bæta við tölvupósti", + "Upgrade": "Uppfæra", + "Verify": "Sannreyna", + "Security": "Öryggi", + "Trusted": "Traustað", + "Subscribe": "Skrá", + "Unsubscribe": "Afskrá", + "None": "Ekkert", + "Ignored/Blocked": "Hunsað/Hindrað", + "Trust": "Treysta", + "Flags": "Fánar", + "Symbols": "Tákn", + "Objects": "Hlutir", + "Activities": "Starfsemi", + "Document": "Skjal", + "Complete": "Búið", + "View": "Skoða", + "Preview": "Forskoðun", + "Strikethrough": "Yfirstrikletrað", + "Italics": "Skáletrað", + "Bold": "Feitletrað", + "ID": "Auðkenni (ID)", + "Disconnect": "Aftengja", + "Share": "Deila", + "Revoke": "Afturkalla", + "Discovery": "Uppgötvun", + "Actions": "Aðgerðir", + "Messages": "Skilaboð", + "Summary": "Yfirlit", + "Service": "Þjónusta", + "Removing…": "Er að fjarlægja…", + "Browse": "Skoða", + "Reset": "Endursetja", + "Sounds": "Hljóð", + "edited": "breytt", + "Re-join": "Taka þátt aftur", + "Banana": "Banani", + "Fire": "Eldur", + "Cloud": "Ský", + "Moon": "Tungl", + "Globe": "Heiminn", + "Mushroom": "Sveppur", + "Cactus": "Kaktus", + "Tree": "Tré", + "Flower": "Blóm", + "Butterfly": "Fiðrildi", + "Octopus": "Kolkrabbi", + "Fish": "Fiskur", + "Turtle": "Skjaldbaka", + "Penguin": "Mörgæs", + "Rooster": "Hani", + "Panda": "Pandabjörn", + "Rabbit": "Kanína", + "Elephant": "Fíll", + "Pig": "Svín", + "Unicorn": "Einhyrningur", + "Horse": "Hestur", + "Lion": "Ljón", + "Cat": "Köttur", + "Dog": "Hundur", + "Guest": "Gestur", + "Other": "Annað", + "Confirm": "Staðfesta", + "Username": "Notandanafn", + "Join": "Taka þátt", + "Encrypted": "Dulkóðað", + "Encryption": "Dulkóðun", + "Timeline": "Tímalína", + "Composer": "Ritari", + "Preferences": "Stillingar", + "Versions": "Útgáfur", + "FAQ": "Algengar spurningar", + "Theme": "Þema", + "General": "Almennt", + "No": "Nei", + "Yes": "Já", + "Verified!": "Sannreynt!", + "Retry": "Reyna aftur", + "Download": "Niðurhal", + "Next": "Næsta", + "Legal": "Löglegt", + "Demote": "Leggja til baka", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)sfór", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sfóru", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sskráðist", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sskráðust", + "Stickerpack": "Límmiða pakki", + "Replying": "Svara", + "%(duration)sd": "%(duration)sd", + "%(duration)sh": "%(duration)sklst", + "%(duration)sm": "%(duration)sm", + "%(duration)ss": "%(duration)ss", + "Emoji picker": "Tjáningartáknmyndvalmynd", + "Show less": "Sýna minna", + "%(count)s messages deleted.|one": "%(count)s skilaboð eytt.", + "%(count)s messages deleted.|other": "%(count)s skilaboðum eytt.", + "Message deleted on %(date)s": "Skilaboð eytt á %(date)s", + "Message edits": "Skilaboðs breytingar" } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 98272f9e49..585ee8ba3a 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3308,5 +3308,72 @@ "Including %(commaSeparatedMembers)s": "Inclusi %(commaSeparatedMembers)s", "View all %(count)s members|one": "Vedi 1 membro", "View all %(count)s members|other": "Vedi tutti i %(count)s membri", - "Failed to send": "Invio fallito" + "Failed to send": "Invio fallito", + "Enter your Security Phrase a second time to confirm it.": "Inserisci di nuovo la password di sicurezza per confermarla.", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Scegli le stanze o le conversazioni da aggiungere. Questo è uno spazio solo per te, nessuno ne saprà nulla. Puoi aggiungerne altre in seguito.", + "What do you want to organise?": "Cosa vuoi organizzare?", + "Filter all spaces": "Filtra tutti gli spazi", + "Delete recording": "Elimina registrazione", + "Stop the recording": "Ferma la registrazione", + "%(count)s results in all spaces|one": "%(count)s risultato in tutti gli spazi", + "%(count)s results in all spaces|other": "%(count)s risultati in tutti gli spazi", + "You have no ignored users.": "Non hai utenti ignorati.", + "Play": "Riproduci", + "Pause": "Pausa", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Questa è una funzione sperimentale. Per ora, i nuovi utenti che ricevono un invito dovranno aprirlo su per entrare.", + "To join %(spaceName)s, turn on the Spaces beta": "Per entrare in %(spaceName)s, attiva la beta degli spazi", + "To view %(spaceName)s, turn on the Spaces beta": "Per vedere %(spaceName)s, attiva la beta degli spazi", + "Select a room below first": "Prima seleziona una stanza sotto", + "Communities are changing to Spaces": "Le comunità stanno diventando spazi", + "Join the beta": "Unisciti alla beta", + "Leave the beta": "Abbandona la beta", + "Beta": "Beta", + "Tap for more info": "Tocca per maggiori info", + "Spaces is a beta feature": "Gli spazi sono una funzionalità beta", + "Want to add a new room instead?": "Vuoi invece aggiungere una nuova stanza?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Aggiunta stanza...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Aggiunta stanze... (%(progress)s di %(count)s)", + "Not all selected were added": "Non tutti i selezionati sono stati aggiunti", + "You can add existing spaces to a space.": "Puoi aggiungere spazi esistenti ad uno spazio.", + "Feeling experimental?": "Ti va di sperimentare?", + "You are not allowed to view this server's rooms list": "Non hai i permessi per vedere l'elenco di stanze del server", + "Error processing voice message": "Errore di elaborazione del vocale", + "We didn't find a microphone on your device. Please check your settings and try again.": "Non abbiamo trovato un microfono nel tuo dispositivo. Controlla le impostazioni e riprova.", + "No microphone found": "Nessun microfono trovato", + "We were unable to access your microphone. Please check your browser settings and try again.": "Non abbiamo potuto accedere al tuo microfono. Controlla le impostazioni del browser e riprova.", + "Unable to access your microphone": "Impossibile accedere al microfono", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Ti va di sperimentare? I laboratori sono il miglior modo di ottenere anteprime, testare nuove funzioni ed aiutare a modellarle prima che vengano pubblicate. Maggiori informazioni.", + "Your access token gives full access to your account. Do not share it with anyone.": "Il tuo token di accesso ti dà l'accesso al tuo account. Non condividerlo con nessuno.", + "Access Token": "Token di accesso", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Gli spazi sono un nuovo modo di raggruppare stanze e persone. Per entrare in uno spazio esistente ti serve un invito.", + "Please enter a name for the space": "Inserisci un nome per lo spazio", + "Connecting": "In connessione", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Permetti Peer-to-Peer per chiamate 1:1 (se lo attivi, l'altra parte potrebbe essere in grado di vedere il tuo indirizzo IP)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta disponibile per web, desktop e Android. Alcune funzioni potrebbero non essere disponibili nel tuo homeserver.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Puoi abbandonare la beta quando vuoi dalle impostazioni o toccando un'etichetta beta, come quella sopra.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s si ricaricherà con gli spazi attivati. Le comunità e le etichette personalizzate saranno nascoste.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta disponibile per web, desktop e Android. Grazie per la partecipazione alla beta.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s si ricaricherà con gli spazi disattivati. Le comunità e le etichette personalizzate saranno di nuovo visibili.", + "Spaces are a new way to group rooms and people.": "Gli spazi sono un nuovo modo di raggruppare stanze e persone.", + "Spaces are a beta feature.": "Gli spazi sono una funzionalità beta.", + "Search names and descriptions": "Cerca nomi e descrizioni", + "You may contact me if you have any follow up questions": "Potete contattarmi se avete altre domande", + "To leave the beta, visit your settings.": "Per abbandonare la beta, vai nelle impostazioni.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Verranno annotate la tua piattaforma e il nome utente per aiutarci ad usare la tua opinione al meglio.", + "%(featureName)s beta feedback": "Feedback %(featureName)s beta", + "Thank you for your feedback, we really appreciate it.": "Grazie per la tua opinione, lo appreziamo molto.", + "Beta feedback": "Feedback beta", + "Add reaction": "Aggiungi reazione", + "Send and receive voice messages": "Invia e ricevi messaggi vocali", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "La tua opinione aiuterà a migliorare gli spazi. Più dettagli dai, meglio è.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se esci, %(brand)s si ricaricherà con gli spazi disattivati. Le comunità e le etichette personalizzate saranno di nuovo visibili.", + "Message search initialisation failed": "Inizializzazione ricerca messaggi fallita", + "Space Autocomplete": "Autocompletamento spazio", + "Go to my space": "Vai nel mio spazio", + "sends space invaders": "invia space invaders", + "Sends the given message with a space themed effect": "Invia il messaggio con un effetto a tema spaziale", + "Kick, ban, or invite people to your active room, and make you leave": "Buttare fuori, bandire o invitare persone nella tua stanza attiva e farti uscire", + "See when people join, leave, or are invited to this room": "Vedere quando le persone entrano, escono o sono invitate in questa stanza", + "Kick, ban, or invite people to this room, and make you leave": "Buttare fuori, bandire o invitare persone in questa stanza e farti uscire", + "See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva" } diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 83d8961147..495e240051 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -351,7 +351,7 @@ "Mirror local video feed": "ローカルビデオ映像送信", "Send analytics data": "分析データを送信する", "Enable inline URL previews by default": "デフォルトでインライン URL プレビューを有効にする", - "Enable URL previews for this room (only affects you)": "この部屋の URL プレビューを有効にする (あなたにのみ影響する)", + "Enable URL previews for this room (only affects you)": "この部屋の URL プレビューを有効にする (あなたにのみ適用)", "Enable URL previews by default for participants in this room": "この部屋の参加者のためにデフォルトで URL プレビューを有効にする", "Room Colour": "部屋の色", "Enable widget screenshots on supported widgets": "サポートされているウィジェットでウィジェットのスクリーンショットを有効にする", @@ -502,10 +502,10 @@ "You have disabled URL previews by default.": "デフォルトで URL プレビューが無効です。", "URL previews are enabled by default for participants in this room.": "この部屋の参加者は、デフォルトで URL プレビューが有効です。", "URL previews are disabled by default for participants in this room.": "この部屋の参加者は、デフォルトで URL プレビューが無効です。", - "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.": "このような暗号化された部屋では、URL プレビューはデフォルトで無効になっており、あなたのホームサーバー(プレビューを作成する場所)がこの部屋に表示されているリンクに関する情報を収集できないようにしています。", + "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.": "この部屋のように暗号化された部屋では、URL プレビューはデフォルトで無効になっています。あなたのホームサーバー (プレビューを作成する) にこの部屋でやり取りされたリンクの情報を収集されないようにするためです。", "URL Previews": "URL プレビュー", "Historical": "履歴のある", - "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.": "メッセージにURLを入力すると、URLプレビューが表示され、タイトル、説明、ウェブサイトからの画像など、そのリンクに関する詳細情報が表示されます。", + "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.": "メッセージに URL が含まれる場合、タイトル、説明、ウェブサイトの画像などが URL プレビューとして表示されます。", "Error decrypting audio": "オーディオの復号化エラー", "Error decrypting attachment": "添付ファイルの復号化エラー", "Decrypt %(text)s": "%(text)s を復号", @@ -629,7 +629,7 @@ "ex. @bob:example.com": "例 @bob:example.com", "Add User": "ユーザーを追加", "Matrix ID": "Matirx ID", - "Matrix Room ID": "Matrix 部屋ID", + "Matrix Room ID": "Matrix 部屋 ID", "email address": "メールアドレス", "You have entered an invalid address.": "無効なアドレスを入力しました。", "Try using one of the following valid address types: %(validTypesList)s.": "次の有効なアドレスタイプのいずれかを使用してください:%(validTypesList)s", @@ -753,8 +753,8 @@ "Community %(groupId)s not found": "コミュニティ %(groupId)s が見つかりません", "Failed to load %(groupId)s": "%(groupId)s をロードできませんでした", "Failed to reject invitation": "招待を拒否できませんでした", - "This room is not public. You will not be able to rejoin without an invite.": "この部屋は公開されていません。 あなたは招待なしで再び参加することはできません。", - "Are you sure you want to leave the room '%(roomName)s'?": "本当にこの部屋「%(roomName)s」から退出してよろしいですか?", + "This room is not public. You will not be able to rejoin without an invite.": "この部屋は公開されていません。再度参加するには、招待が必要です。", + "Are you sure you want to leave the room '%(roomName)s'?": "この部屋「%(roomName)s」から退出してよろしいですか?", "Failed to leave room": "部屋からの退出に失敗しました", "Can't leave Server Notices room": "サーバー通知部屋を離れることはできません", "This room is used for important messages from the Homeserver, so you cannot leave it.": "この部屋はホームサーバーからの重要なメッセージに使用されるため、そこを離れることはできません。", @@ -1212,7 +1212,7 @@ "WARNING: Session already verified, but keys do NOT MATCH!": "警告: このセッションは検証済みです、しかし鍵が一致していません!", "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "警告: 鍵の検証に失敗しました!提供された鍵「%(fingerprint)s」は、%(userId)s およびセッション %(deviceId)s の署名鍵「%(fprint)s」と一致しません。これはつまり、あなたの会話が傍受・盗聴されようとしている恐れがあるということです!", "Show typing notifications": "入力中通知を表示する", - "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "あなたのホームサーバーが対応していない場合は (通話中に自己の IP アドレスが相手に共有されるのを防ぐために) 代替通話支援サーバー turn.matrix.org の使用を許可する", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "あなたのホームサーバーが対応していない場合は代替通話支援サーバー turn.matrix.org の使用を許可 (あなたの IP アドレスが通話相手に漏洩するのを防ぎます)", "Your homeserver does not support cross-signing.": "あなたのホームサーバーはクロス署名に対応していません。", "Cross-signing and secret storage are enabled.": "クロス署名および機密ストレージは有効です。", "Reset cross-signing and secret storage": "クロス署名および機密ストレージをリセット", @@ -2406,7 +2406,7 @@ "Suggested Rooms": "おすすめの部屋", "Explore space rooms": "スペース内の部屋を探索します", "You do not have permissions to add rooms to this space": "このスペースに部屋を追加する権限がありません", - "Add existing room": "既存の部屋を追加します", + "Add existing room": "既存の部屋を追加", "You do not have permissions to create new rooms in this space": "このスペースに新しい部屋を作成する権限がありません", "Send message": "メッセージを送ります", "Invite to this space": "このスペースに招待します", @@ -2417,8 +2417,8 @@ "Space options": "スペースのオプション", "Space Home": "スペースのホーム", "New room": "新しい部屋", - "Leave space": "スペースを離れる", - "Invite people": "人々を招待する", + "Leave space": "スペースを退出", + "Invite people": "人々を招待", "Share your public space": "公開スペースを共有する", "Invite members": "参加者を招待する", "Invite by email or username": "メールまたはユーザー名で招待する", @@ -2440,7 +2440,7 @@ "Create a space": "スペースを作成する", "Delete": "削除", "Jump to the bottom of the timeline when you send a message": "メッセージを送信する際にタイムライン最下部に移動します", - "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spacesはプロトタイプです。 コミュニティ、コミュニティv2、カスタムタグとは互換性がありません。 一部の機能には互換性のあるホームサーバーが必要です。", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "スペースはプロトタイプです。 コミュニティ、コミュニティv2、カスタムタグとは互換性がありません。 一部の機能には互換性のあるホームサーバーが必要です。", "This homeserver has been blocked by it's administrator.": "このホームサーバーは管理者によりブロックされています。", "This homeserver has been blocked by its administrator.": "このホームサーバーは管理者によりブロックされています。", "You're already in a call with this person.": "あなたは既にこの人と通話中です。", @@ -2448,5 +2448,59 @@ "Invite People": "ユーザーを招待", "Edit devices": "デバイスを編集", "%(count)s messages deleted.|one": "%(count)s 件のメッセージが削除されました。", - "%(count)s messages deleted.|other": "%(count)s 件のメッセージが削除されました。" + "%(count)s messages deleted.|other": "%(count)s 件のメッセージが削除されました。", + "To view %(spaceName)s, turn on the Spaces beta": "スペース Beta を有効にすると %(spaceName)s を表示できます", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "1 対 1 の通話で P2P の使用を許可 (有効にするとあなたの IP アドレスが通話相手に漏洩する可能性があります)", + "You have no ignored users.": "無視しているユーザーはいません。", + "Join the beta": "Beta に参加", + "Leave the beta": "Beta を終了", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta は、ウェブ、デスクトップ、Android で利用可能です。Beta をお試しいただきありがとうございます。", + "Your access token gives full access to your account. Do not share it with anyone.": "アクセストークンを用いるとあなたのアカウントの全てにアクセスできます。外部に公開しないでください。", + "Access Token": "アクセストークン", + "Filter all spaces": "全スペースを検索", + "Save Changes": "変更を保存", + "Edit settings relating to your space.": "スペースの設定を変更します。", + "Space settings": "スペースの設定", + "Spaces are a beta feature.": "スペースは Beta 機能です。", + "Spaces is a beta feature": "スペースは Beta 機能です", + "Spaces are a new way to group rooms and people.": "スペースは、部屋や人をグループ化する新しい方法です。", + "Spaces": "スペース", + "Welcome to ": "ようこそ ", + "Invite to just this room": "この部屋に招待", + "Invite to %(spaceName)s": "%(spaceName)s に招待", + "Quick actions": "クイックアクション", + "A private space for you and your teammates": "あなたとチームメイトのプライベートスペース", + "Me and my teammates": "自分とチームメイト", + "Just me": "自分専用", + "Make sure the right people have access to %(name)s": "必要な人が %(name)s にアクセスできるようにします", + "Who are you working with?": "誰が使いますか?", + "Beta": "Beta", + "Tap for more info": "タップして詳細を表示", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "スペースは、部屋や人をグループ化する新しい方法です。既存のスペースに参加するには、招待が必要です。", + "Check your devices": "デバイスを確認", + "Invite to %(roomName)s": "%(roomName)s へ招待", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta は、ウェブ、デスクトップ、Android で利用可能です。お使いのホームサーバーによっては一部機能が利用できない場合があります。", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s はスペースが有効な状態で再読み込みされます。コミュニティとカスタムタグは非表示になります。", + "Communities are changing to Spaces": "コミュニティはスペースに生まれ変わります", + "Beta feedback": "Beta フィードバック", + "%(featureName)s beta feedback": "%(featureName)s Beta フィードバック", + "Send feedback": "フィードバックを送信", + "Manage & explore rooms": "部屋の管理および検索", + "Select a room below first": "以下から部屋を選択してください", + "A private space to organise your rooms": "部屋を整理するためのプライベートスペース", + "Private space": "プライベートスペース", + "Leave Space": "スペースを退出", + "Make this space private": "このスペースを非公開にする", + "Welcome %(name)s": "ようこそ、%(name)s", + "Are you sure you want to leave the space '%(spaceName)s'?": "このスペース「%(spaceName)s」から退出してよろしいですか?", + "This space is not public. You will not be able to rejoin without an invite.": "このスペースは公開されていません。再度参加するには、招待が必要です。", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "この部屋のメンバーはあなただけです。あなたが退出すると、今後あなたを含めて誰もこの部屋に参加できなくなります。", + "Adding rooms... (%(progress)s out of %(count)s)|one": "部屋を追加中...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "部屋を追加中... (%(progress)s / %(count)s)", + "Skip for now": "スキップ", + "What do you want to organise?": "どれを追加しますか?", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "部屋や会話を追加できます。これはあなた専用のスペースで、他の人からは見えません。後から部屋や会話を追加することもできます。", + "Support": "サポート", + "You can change these anytime.": "ここで入力した情報はいつでも編集できます。", + "Add some details to help people recognise it.": "情報を入力してください。" } diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index ed7da7dc6b..b56599f26e 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -1581,5 +1581,6 @@ "Failed to set topic": "Neizdevās iestatīt tematu", "Upload files": "Failu augšupielāde", "These files are too large to upload. The file size limit is %(limit)s.": "Šie faili pārsniedz augšupielādes izmēra limitu %(limit)s.", - "Upload files (%(current)s of %(total)s)": "Failu augšupielāde (%(current)s no %(total)s)" + "Upload files (%(current)s of %(total)s)": "Failu augšupielāde (%(current)s no %(total)s)", + "Check your devices": "Pārskatiet savas ierīces" } diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 8b2dea7885..16f74e7b2d 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -6,7 +6,7 @@ "Admin": "Beheerder", "Advanced": "Geavanceerd", "Always show message timestamps": "Altijd tijdstempels van berichten tonen", - "Authentication": "Authenticatie", + "Authentication": "Login bevestigen", "%(items)s and %(lastItem)s": "%(items)s en %(lastItem)s", "and %(count)s others...|other": "en %(count)s anderen…", "and %(count)s others...|one": "en één andere…", @@ -59,7 +59,7 @@ "Close": "Sluiten", "Create new room": "Nieuw gesprek aanmaken", "Custom Server Options": "Aangepaste serverinstellingen", - "Dismiss": "Afwijzen", + "Dismiss": "Sluiten", "Error": "Fout", "Failed to forget room %(errCode)s": "Vergeten van gesprek is mislukt %(errCode)s", "Favourite": "Favoriet", @@ -171,13 +171,13 @@ "Fill screen": "Scherm vullen", "Filter room members": "Gespreksleden filteren", "Forget room": "Gesprek vergeten", - "For security, this session has been signed out. Please sign in again.": "Wegens veiligheidsredenen is deze sessie afgemeld. Gelieve u opnieuw aan te melden.", + "For security, this session has been signed out. Please sign in again.": "Wegens veiligheidsredenen is deze sessie uitgelogd. Gelieve opnieuw inloggen.", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s van %(fromPowerLevel)s naar %(toPowerLevel)s", "Guests cannot join this room even if explicitly invited.": "Gasten - zelfs speficiek uitgenodigde - kunnen niet aan dit gesprek deelnemen.", "Hangup": "Ophangen", "Historical": "Historisch", "Home": "Thuis", - "Homeserver is": "Thuisserver is", + "Homeserver is": "Homeserver is", "Identity Server is": "Identiteitsserver is", "I have verified my email address": "Ik heb mijn e-mailadres geverifieerd", "Import": "Inlezen", @@ -193,7 +193,7 @@ "Invited": "Uitgenodigd", "Invites": "Uitnodigingen", "Invites user with given id to current room": "Nodigt de gebruiker met de gegeven ID uit in het huidige gesprek", - "Sign in with": "Aanmelden met", + "Sign in with": "Inloggen met", "Join as voice or video.": "Deelnemen met spraak of video.", "Join Room": "Gesprek toetreden", "%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.", @@ -240,7 +240,7 @@ "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s heeft %(targetDisplayName)s in het gesprek uitgenodigd.", "Server error": "Serverfout", "Server may be unavailable, overloaded, or search timed out :(": "De server is misschien onbereikbaar of overbelast, of het zoeken duurde te lang :(", - "Server may be unavailable, overloaded, or you hit a bug.": "De server is misschien onbereikbaar of overbelast, of je bent een fout tegengekomen.", + "Server may be unavailable, overloaded, or you hit a bug.": "De server is misschien onbereikbaar of overbelast, of je bent een bug tegengekomen.", "Server unavailable, overloaded, or something else went wrong.": "De server is onbereikbaar of overbelast, of er is iets anders foutgegaan.", "Session ID": "Sessie-ID", "%(senderName)s kicked %(targetName)s.": "%(senderName)s heeft %(targetName)s het gesprek uitgestuurd.", @@ -249,9 +249,9 @@ "%(senderName)s set a profile picture.": "%(senderName)s heeft een profielfoto ingesteld.", "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s heeft %(displayName)s als weergavenaam aangenomen.", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Tijd in 12-uursformaat tonen (bv. 2:30pm)", - "Signed Out": "Afgemeld", - "Sign in": "Aanmelden", - "Sign out": "Afmelden", + "Signed Out": "Uitgelogd", + "Sign in": "Inloggen", + "Sign out": "Uitloggen", "%(count)s of your messages have not been sent.|other": "Enkele van uw berichten zijn niet verstuurd.", "Someone": "Iemand", "The phone number entered looks invalid": "Het ingevoerde telefoonnummer ziet er ongeldig uit", @@ -266,7 +266,7 @@ "This room": "Dit gesprek", "This room is not accessible by remote Matrix servers": "Dit gesprek is niet toegankelijk vanaf externe Matrix-servers", "To use it, just wait for autocomplete results to load and tab through them.": "Om het te gebruiken, wacht u tot de autoaanvullen resultaten geladen zijn en tabt u erdoorheen.", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "U heeft gepoogd een gegeven punt in de tijdslijn van dit gesprek te laden, maar u bent niet bevoegd het desbetreffende bericht te zien.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "U probeert een punt in de tijdlijn van dit gesprek te laden, maar u heeft niet voldoende rechten om het bericht te lezen.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Geprobeerd een gegeven punt in de tijdslijn van dit gesprek te laden, maar kon dit niet vinden.", "Unable to add email address": "Kan e-mailadres niet toevoegen", "Unable to remove contact information": "Kan contactinformatie niet verwijderen", @@ -329,7 +329,7 @@ "Please select the destination room for this message": "Selecteer het bestemmingsgesprek voor dit bericht", "New Password": "Nieuw wachtwoord", "Start automatically after system login": "Automatisch starten na systeemlogin", - "Analytics": "Statistische gegevens", + "Analytics": "Gebruiksgegevens", "Options": "Opties", "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s verzamelt anonieme analysegegevens die het mogelijk maken de toepassing te verbeteren.", "Passphrases must match": "Wachtwoorden moeten overeenkomen", @@ -350,8 +350,8 @@ "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Weet u zeker dat u deze gebeurtenis wilt verwijderen? Besef wel dat het verwijderen van een van een gespreksnaams- of onderwerpswijziging die wijziging mogelijk teniet doet.", "Unknown error": "Onbekende fout", "Incorrect password": "Onjuist wachtwoord", - "Unable to restore session": "Sessieherstel lukt niet", - "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "Als u reeds een recentere versie van %(brand)s heeft gebruikt is uw sessie mogelijk onverenigbaar met deze versie. Sluit dit venster en ga terug naar die recentere versie.", + "Unable to restore session": "Herstellen van sessie mislukt", + "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "Als u een recentere versie van %(brand)s heeft gebruikt is uw sessie mogelijk niet geschikt voor deze versie. Sluit dit venster en ga terug naar die recentere versie.", "Unknown Address": "Onbekend adres", "ex. @bob:example.com": "bv. @jan:voorbeeld.com", "Add User": "Gebruiker toevoegen", @@ -628,10 +628,10 @@ "Room Notification": "Groepsgespreksmelding", "The information being sent to us to help make %(brand)s better includes:": "De informatie die naar ons wordt verstuurd om %(brand)s te verbeteren bevat:", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Waar deze pagina identificeerbare informatie bevat, zoals een gespreks-, gebruikers- of groeps-ID, zullen deze gegevens verwijderd worden voordat ze naar de server gestuurd worden.", - "The platform you're on": "Het platform dat je gebruikt", + "The platform you're on": "Het platform dat u gebruikt", "The version of %(brand)s": "De versie van %(brand)s", "Your language of choice": "De door jou gekozen taal", - "Which officially provided instance you are using, if any": "Welke officieel aangeboden instantie je eventueel gebruikt", + "Which officially provided instance you are using, if any": "Welke officieel aangeboden instantie u eventueel gebruikt", "Whether or not you're using the Richtext mode of the Rich Text Editor": "Of u de tekstverwerker al dan niet in de modus voor opgemaakte tekst gebruikt", "Your homeserver's URL": "De URL van je homeserver", "In reply to ": "Als antwoord op ", @@ -658,8 +658,8 @@ "Who can join this community?": "Wie kan er tot deze gemeenschap toetreden?", "Everyone": "Iedereen", "Leave this community": "Deze gemeenschap verlaten", - "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Voor het oplossen van, via GitHub, gemelde problemen helpen foutopsporingslogboeken ons enorm. Deze bevatten wel gebruiksgegevens (waaronder uw gebruikersnaam, de ID’s of bijnamen van de gesprekken en groepen die u heeft bezocht, en de namen van andere gebruikers), maar geen berichten.", - "Submit debug logs": "Foutopsporingslogboeken indienen", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Voor het oplossen van, via GitHub, gemelde bugs helpen foutenlogboeken ons enorm. Deze bevatten wel uw gebruiksgegevens, maar geen berichten. Het bevat onder meer uw gebruikersnaam, de ID’s of bijnamen van de gesprekken en groepen die u heeft bezocht en de namen van andere gebruikers.", + "Submit debug logs": "Foutenlogboek versturen", "Opens the Developer Tools dialog": "Opent het dialoogvenster met ontwikkelaarsgereedschap", "Fetching third party location failed": "Het ophalen van de locatie van de derde partij is mislukt", "I understand the risks and wish to continue": "Ik begrijp de risico’s en wil graag verdergaan", @@ -727,11 +727,11 @@ "All messages (noisy)": "Alle berichten (luid)", "Enable them now": "Deze nu inschakelen", "Toolbox": "Gereedschap", - "Collecting logs": "Logboeken worden verzameld", + "Collecting logs": "Logs worden verzameld", "You must specify an event type!": "U dient een gebeurtenistype op te geven!", "(HTTP status %(httpStatus)s)": "(HTTP-status %(httpStatus)s)", "Invite to this room": "Uitnodigen voor dit gesprek", - "Send logs": "Logboeken versturen", + "Send logs": "Logs versturen", "All messages": "Alle berichten", "Call invitation": "Oproep-uitnodiging", "Downloading update...": "Update wordt gedownload…", @@ -778,17 +778,17 @@ "Thank you!": "Bedankt!", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Met uw huidige browser kan de toepassing er volledig onjuist uitzien. Tevens is het mogelijk dat niet alle functies naar behoren werken. U kunt doorgaan als u het toch wilt proberen, maar bij problemen bent u volledig op uzelf aangewezen!", "Checking for an update...": "Bezig met controleren op updates…", - "Logs sent": "Logboeken verstuurd", - "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Foutopsporingslogboeken bevatten gebruiksgegevens over de toepassing, inclusief uw gebruikersnaam, de ID’s of bijnamen van de gesprekken die u heeft bezocht, evenals de gebruikersnamen van andere gebruikers. Ze bevatten geen berichten.", - "Failed to send logs: ": "Versturen van logboeken mislukt: ", - "Preparing to send logs": "Logboeken worden voorbereid voor versturen", + "Logs sent": "Logs verstuurd", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Foutenlogboeken bevatten gebruiksgegevens van de app inclusief uw gebruikersnaam, de ID’s of bijnamen van de gesprekken die u heeft bezocht, en de gebruikersnamen van andere gebruikers. Ze bevatten geen berichten.", + "Failed to send logs: ": "Versturen van logs mislukt: ", + "Preparing to send logs": "Logs voorbereiden voor versturen", "e.g. %(exampleValue)s": "bv. %(exampleValue)s", - "Every page you use in the app": "Iedere bladzijde die je in de toepassing gebruikt", + "Every page you use in the app": "Iedere bladzijde die u in de app gebruikt", "e.g. ": "bv. ", "Your device resolution": "De resolutie van je apparaat", "Missing roomId.": "roomId ontbreekt.", "Always show encryption icons": "Versleutelingspictogrammen altijd tonen", - "Send analytics data": "Statistische gegevens versturen", + "Send analytics data": "Gebruiksgegevens delen", "Enable widget screenshots on supported widgets": "Widget-schermafbeeldingen inschakelen op ondersteunde widgets", "Muted Users": "Gedempte gebruikers", "Popout widget": "Widget in nieuw venster openen", @@ -798,11 +798,11 @@ "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "De zichtbaarheid van berichten in Matrix is zoals bij e-mails. Het vergeten van uw berichten betekent dat berichten die u heeft verstuurd niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde gebruikers die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan.", "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Vergeet bij het sluiten van mijn account alle door mij verstuurde berichten (Let op: hierdoor zullen personen een onvolledig beeld krijgen van gesprekken)", "To continue, please enter your password:": "Voer uw wachtwoord in om verder te gaan:", - "Clear Storage and Sign Out": "Opslag wissen en afmelden", - "Send Logs": "Logboek versturen", + "Clear Storage and Sign Out": "Opslag wissen en uitloggen", + "Send Logs": "Logs versturen", "Refresh": "Herladen", "We encountered an error trying to restore your previous session.": "Het herstel van uw vorige sessie is mislukt.", - "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Het legen van de opslag van uw browser zal het probleem misschien verhelpen, maar zal u ook afmelden en uw gehele versleutelde gespreksgeschiedenis onleesbaar maken.", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Het wissen van de browseropslag zal het probleem misschien verhelpen, maar zal u ook uitloggen en uw gehele versleutelde gespreksgeschiedenis onleesbaar maken.", "Collapse Reply Thread": "Reactieketting dichtvouwen", "Can't leave Server Notices room": "Kan servermeldingsgesprek niet verlaten", "This room is used for important messages from the Homeserver, so you cannot leave it.": "Dit gesprek is bedoeld voor belangrijke berichten van de homeserver, dus u kunt het niet verlaten.", @@ -842,7 +842,7 @@ "Bulk options": "Bulkopties", "This homeserver has hit its Monthly Active User limit.": "Deze homeserver heeft zijn limiet voor maandelijks actieve gebruikers bereikt.", "This homeserver has exceeded one of its resource limits.": "Deze homeserver heeft één van zijn systeembronlimieten overschreden.", - "Whether or not you're logged in (we don't record your username)": "Of je al dan niet ingelogd bent (we slaan je gebruikersnaam niet op)", + "Whether or not you're logged in (we don't record your username)": "Of u al dan niet ingelogd bent (we slaan je gebruikersnaam niet op)", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Het bestand ‘%(fileName)s’ is groter dan de uploadlimiet van de homeserver", "Unable to load! Check your network connectivity and try again.": "Laden mislukt! Controleer je netwerktoegang en probeer het nogmaals.", "Failed to invite users to the room:": "Kon de volgende gebruikers hier niet uitnodigen:", @@ -1023,20 +1023,20 @@ "Profile picture": "Profielfoto", "Upgrade to your own domain": "Upgrade naar uw eigen domein", "Display Name": "Weergavenaam", - "Set a new account password...": "Stel een nieuw accountwachtwoord in…", + "Set a new account password...": "Stel een nieuw wachtwoord in…", "Email addresses": "E-mailadressen", "Phone numbers": "Telefoonnummers", "Language and region": "Taal en regio", "Theme": "Thema", "Account management": "Accountbeheer", - "Deactivating your account is a permanent action - be careful!": "Pas op! Het sluiten van uw account is onherroepelijk!", + "Deactivating your account is a permanent action - be careful!": "Pas op! Het sluiten van uw account kan niet teruggedraaid worden!", "General": "Algemeen", - "Legal": "Wettelijk", + "Legal": "Juridisch", "Credits": "Met dank aan", "For help with using %(brand)s, click here.": "Klik hier voor hulp bij het gebruiken van %(brand)s.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Klik hier voor hulp bij het gebruiken van %(brand)s, of begin een gesprek met onze robot met de knop hieronder.", - "Help & About": "Hulp & Info", - "Bug reporting": "Foutmeldingen", + "Help & About": "Hulp & info", + "Bug reporting": "Bug meldingen", "FAQ": "FAQ", "Versions": "Versies", "Preferences": "Instellingen", @@ -1046,10 +1046,10 @@ "Autocomplete delay (ms)": "Vertraging voor autoaanvullen (ms)", "Accept all %(invitedRooms)s invites": "Alle %(invitedRooms)s de uitnodigingen aannemen", "Key backup": "Sleutelback-up", - "Security & Privacy": "Veiligheid & Privacy", + "Security & Privacy": "Veiligheid & privacy", "Missing media permissions, click the button below to request.": "Mediatoestemmingen ontbreken, klik op de knop hieronder om deze aan te vragen.", "Request media permissions": "Mediatoestemmingen verzoeken", - "Voice & Video": "Spraak & Video", + "Voice & Video": "Spraak & video", "Room information": "Gespreksinformatie", "Internal room ID:": "Interne gespreks-ID:", "Room version": "Gespreksversie", @@ -1108,7 +1108,7 @@ "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Kan geen profielen voor de Matrix-ID’s hieronder vinden - wilt u ze toch uitnodigen?", "Invite anyway and never warn me again": "Alsnog uitnodigen en mij nooit meer waarschuwen", "Invite anyway": "Alsnog uitnodigen", - "Before submitting logs, you must create a GitHub issue to describe your problem.": "Voor u logboeken indient, dient u uw probleem te melden op GitHub.", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Voordat u logs indient, dient u uw probleem te melden in een GitHub issue.", "Unable to load commit detail: %(msg)s": "Kan commitdetail niet laden: %(msg)s", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Om uw gespreksgeschiedenis niet te verliezen vóór het uitloggen dient u uw veiligheidssleutel te exporteren. Dat moet vanuit de nieuwere versie van %(brand)s", "Incompatible Database": "Incompatibele database", @@ -1125,7 +1125,7 @@ "I don't want my encrypted messages": "Ik wil mijn versleutelde berichten niet", "Manually export keys": "Sleutels handmatig wegschrijven", "You'll lose access to your encrypted messages": "U zult de toegang tot uw versleutelde berichten verliezen", - "Are you sure you want to sign out?": "Weet u zeker dat u zich wilt afmelden?", + "Are you sure you want to sign out?": "Weet u zeker dat u wilt uitloggen?", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Als u fouten zou tegenkomen of voorstellen zou hebben, laat het ons dan weten op GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "Voorkom dubbele meldingen: doorzoek eerst de bestaande meldingen (en voeg desgewenst een +1 toe). Maak enkel een nieuwe melding aan indien u niets kunt vinden.", "Report bugs & give feedback": "Fouten melden & feedback geven", @@ -1193,7 +1193,7 @@ "Could not load user profile": "Kon gebruikersprofiel niet laden", "Your Matrix account on %(serverName)s": "Uw Matrix-account op %(serverName)s", "A verification email will be sent to your inbox to confirm setting your new password.": "Er is een verificatie-e-mail naar u gestuurd om het instellen van uw nieuwe wachtwoord te bevestigen.", - "Sign in instead": "Aanmelden", + "Sign in instead": "In plaats daarvan inloggen", "Your password has been reset.": "Uw wachtwoord is opnieuw ingesteld.", "Set a new password": "Stel een nieuw wachtwoord in", "Invalid homeserver discovery response": "Ongeldig homeserver-vindbaarheids-antwoord", @@ -1202,13 +1202,13 @@ "This homeserver does not support login using email address.": "Deze homeserver biedt geen ondersteuning voor inloggen met e-mailadres.", "Please contact your service administrator to continue using this service.": "Gelieve contact op te nemen met uw dienstbeheerder om deze dienst te blijven gebruiken.", "Failed to perform homeserver discovery": "Ontdekken van homeserver is mislukt", - "Sign in with single sign-on": "Aanmelden met eenmalige aanmelding", - "Create account": "Account aanmaken", + "Sign in with single sign-on": "Inloggen met eenmalig inloggen", + "Create account": "Registeren", "Registration has been disabled on this homeserver.": "Registratie is uitgeschakeld op deze homeserver.", "Unable to query for supported registration methods.": "Kan ondersteunde registratiemethoden niet opvragen.", "Create your account": "Maak uw account aan", "Keep going...": "Doe verder…", - "For maximum security, this should be different from your account password.": "Voor maximale veiligheid zou dit moeten verschillen van uw accountwachtwoord.", + "For maximum security, this should be different from your account password.": "Voor maximale veiligheid moet dit verschillen van uw accountwachtwoord.", "That matches!": "Dat komt overeen!", "That doesn't match.": "Dat komt niet overeen.", "Go back to set it again.": "Ga terug om het opnieuw in te stellen.", @@ -1228,11 +1228,11 @@ "Don't ask again": "Niet opnieuw vragen", "New Recovery Method": "Nieuwe herstelmethode", "A new recovery passphrase and key for Secure Messages have been detected.": "Er zijn een nieuw herstelwachtwoord en een nieuwe herstelsleutel voor beveiligde berichten gedetecteerd.", - "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.": "Als u deze nieuwe herstelmethode niet heeft ingesteld, is het mogelijk dat een aanvaller toegang tot uw account probeert te krijgen. Wijzig onmiddellijk uw accountwachtwoord en stel in het instellingenmenu een nieuwe herstelmethode in.", + "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.": "Als u deze nieuwe herstelmethode niet heeft ingesteld, is het mogelijk dat een aanvaller toegang tot uw account probeert te krijgen. Wijzig onmiddellijk uw wachtwoord en stel bij instellingen een nieuwe herstelmethode in.", "Go to Settings": "Ga naar instellingen", "Set up Secure Messages": "Beveiligde berichten instellen", "Recovery Method Removed": "Herstelmethode verwijderd", - "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Als u de herstelmethode niet heeft verwijderd, is het mogelijk dat er een aanvaller toegang tot uw account probeert te verkrijgen. Wijzig onmiddellijk uw accountwachtwoord en stel in het instellingenmenu een nieuwe herstelmethode in.", + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Als u de herstelmethode niet heeft verwijderd, is het mogelijk dat er een aanvaller toegang tot uw account probeert te verkrijgen. Wijzig onmiddellijk uw wachtwoord en stel bij instellingen een nieuwe herstelmethode in.", "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.": "Let op: gesprekken bijwerken voegt gespreksleden niet automatisch toe aan de nieuwe versie van het gesprek. Er komt in het oude gesprek een koppeling naar het nieuwe, waarop gespreksleden moeten klikken om aan het nieuwe gesprek deel te nemen.", "Adds a custom widget by URL to the room": "Voegt met een URL een aangepaste widget toe aan het gesprek", "Please supply a https:// or http:// widget URL": "Voer een https://- of http://-widget-URL in", @@ -1264,8 +1264,8 @@ "GitHub issue": "GitHub-melding", "Notes": "Opmerkingen", "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Gelieve alle verdere informatie die zou kunnen helpen het probleem te analyseren (wat u aan het doen was, relevante gespreks-ID’s, gebruikers-ID’s, enz.) bij te voegen.", - "Sign out and remove encryption keys?": "Afmelden en versleutelingssleutels verwijderen?", - "To help us prevent this in future, please send us logs.": "Gelieve ons logboeken te sturen om dit in de toekomst te helpen voorkomen.", + "Sign out and remove encryption keys?": "Uitloggen en versleutelingssleutels verwijderen?", + "To help us prevent this in future, please send us logs.": "Stuur ons uw logs om dit in de toekomst te helpen voorkomen.", "Missing session data": "Sessiegegevens ontbreken", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Sommige sessiegegevens, waaronder sleutels voor versleutelde berichten, ontbreken. Herstel de sleutels uit uw back-up door u af- en weer aan te melden.", "Your browser likely removed this data when running low on disk space.": "Uw browser heeft deze gegevens wellicht verwijderd toen de beschikbare opslagruimte vol was.", @@ -1297,7 +1297,7 @@ "Rejecting invite …": "Uitnodiging wordt geweigerd…", "Join the conversation with an account": "Neem deel aan het gesprek met een account", "Sign Up": "Registreren", - "Sign In": "Aanmelden", + "Sign In": "Inloggen", "You were kicked from %(roomName)s by %(memberName)s": "U bent uit %(roomName)s gezet door %(memberName)s", "Reason: %(reason)s": "Reden: %(reason)s", "Forget this room": "Dit gesprek vergeten", @@ -1315,7 +1315,7 @@ "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s kan niet vooraf bekeken worden. Wilt u eraan deelnemen?", "This room doesn't exist. Are you sure you're at the right place?": "Dit gesprek bestaat niet. Weet u zeker dat u zich op de juiste plaats bevindt?", "Try again later, or ask a room admin to check if you have access.": "Probeer het later opnieuw, of vraag een gespreksbeheerder om te controleren of u wel toegang heeft.", - "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "De foutcode %(errcode)s is weergegeven bij het toetreden van het gesprek. Als u meent dat u dit bericht foutief te zien krijgt, gelieve dan een foutmelding in te dienen.", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "De foutcode %(errcode)s is weergegeven bij het toetreden van het gesprek. Als u meent dat u dit bericht foutief te zien krijgt, gelieve dan een bugmelding indienen.", "This room has already been upgraded.": "Dit gesprek is reeds geüpgraded.", "reacted with %(shortName)s": "heeft gereageerd met %(shortName)s", "edited": "bewerkt", @@ -1386,11 +1386,11 @@ "Resend edit": "Bewerking opnieuw versturen", "Resend %(unsentCount)s reaction(s)": "%(unsentCount)s reactie(s) opnieuw versturen", "Resend removal": "Verwijdering opnieuw versturen", - "Failed to re-authenticate due to a homeserver problem": "Opnieuw aanmelden is mislukt wegens een probleem met de homeserver", - "Failed to re-authenticate": "Opnieuw aanmelden is mislukt", + "Failed to re-authenticate due to a homeserver problem": "Opnieuw inloggen is mislukt wegens een probleem met de homeserver", + "Failed to re-authenticate": "Opnieuw inloggen is mislukt", "Enter your password to sign in and regain access to your account.": "Voer uw wachtwoord in om u aan te melden en toegang tot uw account te herkrijgen.", "Forgotten your password?": "Wachtwoord vergeten?", - "You're signed out": "U bent afgemeld", + "You're signed out": "U bent uitgelogd", "Clear personal data": "Persoonlijke gegevens wissen", "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Laat ons weten wat er verkeerd is gegaan, of nog beter, maak een foutrapport aan op GitHub, waarin u het probleem beschrijft.", "Identity Server": "Identiteitsserver", @@ -1401,7 +1401,7 @@ "Service": "Dienst", "Summary": "Samenvatting", "Sign in and regain access to your account.": "Meld u aan en herkrijg toegang tot uw account.", - "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt zich niet aanmelden met uw account. Neem voor meer informatie contact op met de beheerder van uw homeserver.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt niet inloggen met uw account. Neem voor meer informatie contact op met de beheerder van uw homeserver.", "This account has been deactivated.": "Deze account is gesloten.", "Messages": "Berichten", "Actions": "Acties", @@ -1478,11 +1478,11 @@ "No recent messages by %(user)s found": "Geen recente berichten door %(user)s gevonden", "Try scrolling up in the timeline to see if there are any earlier ones.": "Probeer omhoog te scrollen in de tijdslijn om te kijken of er eerdere zijn.", "Remove recent messages by %(user)s": "Recente berichten door %(user)s verwijderen", - "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "U staat op het punt %(count)s berichten door %(user)s te verwijderen. Dit is onherroepelijk. Wilt u doorgaan?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "U staat op het punt %(count)s berichten van %(user)s te verwijderen. Dit kan niet teruggedraaid worden. Wilt u doorgaan?", "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Bij een groot aantal berichten kan dit even duren. Herlaad uw cliënt niet gedurende deze tijd.", "Remove %(count)s messages|other": "%(count)s berichten verwijderen", "Deactivate user?": "Gebruiker deactiveren?", - "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deze gebruiker deactiveren zal deze gebruiker uitloggen en verhinderen dat de gebruiker weer inlogt. Bovendien zal de gebruiker alle gesprekken waaraan de gebruiker deelneemt verlaten. Deze actie is onherroepelijk. Weet u zeker dat u deze gebruiker wilt deactiveren?", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deze gebruiker deactiveren zal deze gebruiker uitloggen en verhinderen dat de gebruiker weer inlogt. Bovendien zal de gebruiker alle gesprekken waaraan de gebruiker deelneemt verlaten. Deze actie is niet terug te draaien. Weet u zeker dat u deze gebruiker wilt deactiveren?", "Deactivate user": "Gebruiker deactiveren", "Remove recent messages": "Recente berichten verwijderen", "Bold": "Vet", @@ -1517,13 +1517,13 @@ "Explore rooms": "Gesprekken 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 is onherroepelijk. Wilt u doorgaan?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "U staat op het punt 1 bericht door %(user)s te verwijderen. Dit kan niet ongedaan gemaakt worden. Wilt u doorgaan?", "Remove %(count)s messages|one": "1 bericht verwijderen", "%(count)s unread messages including mentions.|other": "%(count)s ongelezen berichten, inclusief vermeldingen.", "%(count)s unread messages.|other": "%(count)s ongelezen berichten.", "Unread mentions.": "Ongelezen vermeldingen.", "Show image": "Afbeelding tonen", - "Please create a new issue on GitHub so that we can investigate this bug.": "Maak een nieuw rapport aan op GitHub opdat we dit probleem kunnen onderzoeken.", + "Please create a new issue on GitHub so that we can investigate this bug.": "Maak een nieuwe issue aan op GitHub zodat we deze bug kunnen onderzoeken.", "e.g. my-room": "bv. mijn-gesprek", "Close dialog": "Dialoog sluiten", "Please enter a name for the room": "Geef een naam voor het gesprek op", @@ -1549,7 +1549,7 @@ "Click the link in the email you received to verify and then click continue again.": "Open de koppeling in de ontvangen verificatie-e-mail, en klik dan op ‘Doorgaan’.", "%(creator)s created and configured the room.": "Gesprek gestart en ingesteld door %(creator)s.", "Setting up keys": "Sleutelconfiguratie", - "Verify this session": "Deze sessie verifiëren", + "Verify this session": "Verifieer deze sessie", "Encryption upgrade available": "Versleutelingsupgrade beschikbaar", "You can use /help to list available commands. Did you mean to send this as a message?": "Typ /help om alle opdrachten te zien. Was het uw bedoeling dit als bericht te sturen?", "Help": "Hulp", @@ -1621,7 +1621,7 @@ "Upgrade": "Upgraden", "Verify": "Verifiëren", "Later": "Later", - "Review": "Controle", + "Review": "Controleer", "Decline (%(counter)s)": "Afwijzen (%(counter)s)", "This bridge was provisioned by .": "Dank aan voor de brug.", "This bridge is managed by .": "Brug onderhouden door .", @@ -1636,14 +1636,14 @@ "Unable to load session list": "Kan sessielijst niet laden", "Delete %(count)s sessions|other": "%(count)s sessies verwijderen", "Delete %(count)s sessions|one": "%(count)s sessie verwijderen", - "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Of je %(brand)s op een apparaat gebruikt waarop een aanraakscherm de voornaamste invoermethode is", - "Whether you're using %(brand)s as an installed Progressive Web App": "Of je %(brand)s gebruikt als een geïnstalleerde Progressive-Web-App", + "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Of u %(brand)s op een apparaat gebruikt waarop een aanraakscherm de voornaamste invoermethode is", + "Whether you're using %(brand)s as an installed Progressive Web App": "Of u %(brand)s gebruikt als een geïnstalleerde Progressieve Web-App", "Your user agent": "Jouw gebruikersagent", "If you cancel now, you won't complete verifying the other user.": "Als u nu annuleert zult u de andere gebruiker niet verifiëren.", "If you cancel now, you won't complete verifying your other session.": "Als u nu annuleert zult u uw andere sessie niet verifiëren.", "Cancel entering passphrase?": "Wachtwoord annuleren?", "Show typing notifications": "Typmeldingen weergeven", - "Verify this session by completing one of the following:": "Verifieer deze sessie door een van het volgende te doen:", + "Verify this session by completing one of the following:": "Verifieer deze sessie door een van het volgende handelingen te doen:", "Scan this unique code": "Scan deze unieke code", "or": "of", "Compare unique emoji": "Vergelijk unieke emoji", @@ -1689,7 +1689,7 @@ "wait and try again later": "wachten en het later weer proberen", "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Gebruik een integratiebeheerder (%(serverName)s) om robots, widgets en stickerpakketten te beheren.", "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Gebruik een integratiebeheerder om robots, widgets en stickerpakketten te beheren.", - "Manage integrations": "Beheer integraties", + "Manage integrations": "Integratiebeheerder", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integratiebeheerders ontvangen configuratie-informatie en kunnen widgets aanpassen, gespreksuitnodigingen versturen en machtsniveau’s namens u aanpassen.", "Ban list rules - %(roomName)s": "Banlijstregels - %(roomName)s", "Server rules": "Serverregels", @@ -1746,11 +1746,11 @@ "Clear notifications": "Meldingen wissen", "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "U moet uw persoonlijke informatie van de identiteitsserver verwijderen voordat u zich ontkoppelt. Helaas kan de identiteitsserver op dit moment niet worden bereikt. Mogelijk is hij offline.", "Your homeserver does not support cross-signing.": "Uw homeserver biedt geen ondersteuning voor kruiselings ondertekenen.", - "Homeserver feature support:": "Homeserver ondersteund deze functies:", + "Homeserver feature support:": "Homeserver functie ondersteuning:", "exists": "aanwezig", "Sign In or Create Account": "Meld u aan of maak een account aan", "Use your account or create a new one to continue.": "Gebruik uw bestaande account of maak een nieuwe aan om verder te gaan.", - "Create Account": "Account aanmaken", + "Create Account": "Registeren", "Displays information about a user": "Geeft informatie weer over een gebruiker", "Order rooms by name": "Gesprekken sorteren op naam", "Show rooms with unread notifications first": "Gesprekken met ongelezen meldingen eerst tonen", @@ -1872,10 +1872,10 @@ "More options": "Meer opties", "Language Dropdown": "Taalselectie", "Destroy cross-signing keys?": "Sleutels voor kruiselings ondertekenen verwijderen?", - "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Het verwijderen van sleutels voor kruiselings ondertekenen is onherroepelijk. Iedereen waarmee u geverifieerd heeft zal beveiligingswaarschuwingen te zien krijgen. U wilt dit hoogstwaarschijnlijk niet doen, tenzij u alle apparaten heeft verloren waarmee u kruiselings kon ondertekenen.", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Het verwijderen van sleutels voor kruiselings ondertekenen is niet terug te draaien. Iedereen waarmee u geverifieerd heeft zal beveiligingswaarschuwingen te zien krijgen. U wilt dit hoogstwaarschijnlijk niet doen, tenzij u alle apparaten heeft verloren waarmee u kruiselings kon ondertekenen.", "Clear cross-signing keys": "Sleutels voor kruiselings ondertekenen wissen", "Clear all data in this session?": "Alle gegevens in deze sessie verwijderen?", - "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Het verwijderen van alle gegevens in deze sessie is onherroepelijk. Versleutelde berichten zullen verloren gaan, tenzij u een back-up van de sleutels heeft.", + "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Het verwijderen van alle gegevens in deze sessie is niet terug te draaien. Versleutelde berichten zullen verloren gaan, tenzij u een back-up van de sleutels heeft.", "Verify session": "Sessie verifiëren", "Session name": "Sessienaam", "Session key": "Sessiesleutel", @@ -1884,7 +1884,7 @@ "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.": "Verifieer dit apparaat om het als vertrouwd te markeren. Door dit apparaat te vertrouwen geeft u extra gemoedsrust aan uzelf en andere gebruikers bij het gebruik van eind-tot-eind-versleutelde berichten.", "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Dit apparaat verifiëren zal het als vertrouwd markeren, en gebruikers die met u geverifieerd hebben zullen het vertrouwen.", "Integrations are disabled": "Integraties zijn uitgeschakeld", - "Enable 'Manage Integrations' in Settings to do this.": "Schakel in het Algemene Instellingenmenu ‘Beheer integraties’ in om dit te doen.", + "Enable 'Manage Integrations' in Settings to do this.": "Schakel de ‘Integratiebeheerder’ in in uw Instellingen om dit te doen.", "Integrations not allowed": "Integraties niet toegestaan", "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Uw %(brand)s laat u geen integratiebeheerder gebruiken om dit te doen. Neem contact op met een beheerder.", "Failed to invite the following users to chat: %(csvUsers)s": "Het uitnodigen van volgende gebruikers voor gesprek is mislukt: %(csvUsers)s", @@ -1909,7 +1909,7 @@ "Automatically invite users": "Gebruikers automatisch uitnodigen", "Upgrade private room": "Privégesprek upgraden", "Upgrade public room": "Openbaar gesprek upgraden", - "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Het bijwerken van een gesprek is een gevorderde actie en wordt meestal aanbevolen wanneer een gesprek onstabiel is door fouten, ontbrekende functies of problemen met de beveiliging.", + "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Het bijwerken van een gesprek is een gevorderde actie en wordt meestal aanbevolen wanneer een gesprek onstabiel is door bugs, ontbrekende functies of problemen met de beveiliging.", "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "Dit heeft meestal enkel een invloed op de manier waarop het gesprek door de server verwerkt wordt. Als u problemen met uw %(brand)s ondervindt, dien dan een foutmelding in.", "You'll upgrade this room from to .": "U upgrade dit gesprek van naar .", "This will allow you to return to your account after signing out, and sign in on other sessions.": "Daardoor kunt u na afmelding terugkeren tot uw account, en u bij andere sessies aanmelden.", @@ -1927,25 +1927,25 @@ "Remove for me": "Verwijderen voor mezelf", "User Status": "Gebruikersstatus", "Country Dropdown": "Landselectie", - "Confirm your identity by entering your account password below.": "Bevestig uw identiteit door hieronder uw accountwachtwoord in te voeren.", + "Confirm your identity by entering your account password below.": "Bevestig uw identiteit door hieronder uw wachtwoord in te voeren.", "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Er is geen identiteitsserver geconfigureerd, dus u kunt geen e-mailadres toevoegen om in de toekomst een nieuw wachtwoord in te stellen.", "Jump to first unread room.": "Ga naar het eerste ongelezen gesprek.", "Jump to first invite.": "Ga naar de eerste uitnodiging.", "Session verified": "Sessie geverifieerd", - "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. Ze heeft nu toegang tot uw versleutelde berichten, en de sessie zal voor andere gebruikers als vertrouwd gemarkeerd worden.", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. U heeft nu toegang tot uw versleutelde berichten, en deze sessie zal voor andere gebruikers als vertrouwd gemarkeerd worden.", "Your new session is now verified. Other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. Ze zal voor andere gebruikers als vertrouwd gemarkeerd worden.", "Without completing security on this session, it won’t have access to encrypted messages.": "Als u de beveiliging van deze sessie niet vervolledigt, zal ze geen toegang hebben tot uw versleutelde berichten.", "Go Back": "Terugkeren", "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Door uw wachtwoord te wijzigen stelt u alle eind-tot-eind-versleutelingssleutels op al uw sessies opnieuw in, waardoor uw versleutelde gespreksgeschiedenis onleesbaar wordt. Stel uw sleutelback-up in of sla uw gesprekssleutels van een andere sessie op voor u een nieuw wachtwoord instelt.", "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "U bent uitgelogd bij al uw sessies en zult geen pushberichten meer ontvangen. Meld u op elk apparaat opnieuw aan om meldingen opnieuw in te schakelen.", "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Ontvang toegang tot uw account en herstel de tijdens deze sessie opgeslagen versleutelingssleutels, zonder deze sleutels zijn sommige van uw versleutelde berichten in uw sessies onleesbaar.", - "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.": "Let op: uw persoonlijke gegevens (waaronder versleutelingssleutels) zijn nog steeds opgeslagen in deze sessie. Wis ze wanneer u klaar bent met deze sessie, of wanneer u zich wilt aanmelden met een andere account.", + "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.": "Let op: uw persoonlijke gegevens (waaronder versleutelingssleutels) zijn nog steeds opgeslagen in deze sessie. Wis ze wanneer u klaar bent met deze sessie, of wanneer u wilt inloggen met een andere account.", "Command Autocomplete": "Opdrachten autoaanvullen", "DuckDuckGo Results": "DuckDuckGo-resultaten", - "Enter your account password to confirm the upgrade:": "Voer uw accountwachtwoord in om het upgraden te bevestigen:", + "Enter your account password to confirm the upgrade:": "Voer uw wachtwoord in om het upgraden te bevestigen:", "Restore your key backup to upgrade your encryption": "Herstel uw sleutelback-up om uw versleuteling te upgraden", "Restore": "Herstellen", - "You'll need to authenticate with the server to confirm the upgrade.": "U zult zich moeten aanmelden bij de server om het upgraden te bevestigen.", + "You'll need to authenticate with the server to confirm the upgrade.": "U zult moeten inloggen bij de server om het upgraden te bevestigen.", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade deze sessie om er andere sessies mee te verifiëren, waardoor deze ook de toegang verkrijgen tot uw versleutelde berichten en deze voor andere gebruikers als vertrouwd gemarkeerd worden.", "Set up with a recovery key": "Instellen met een herstelsleutel", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Bewaar een kopie op een veilige plaats, zoals in een wachtwoordbeheerder of een kluis.", @@ -1970,7 +1970,7 @@ "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "Bekijk eerst het beveiligingsopenbaarmakingsbeleid van Matrix.org als u een probleem met de beveiliging van Matrix wilt melden.", "Not currently indexing messages for any room.": "Er worden momenteel voor geen enkel gesprek berichten geïndexeerd.", "%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s van %(totalRooms)s", - "Where you’re logged in": "Waar u ingelogd bent", + "Where you’re logged in": "Waar u bent ingelogd", "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Beheer hieronder de namen van uw sessies en meld ze af. Of verifieer ze in uw gebruikersprofiel.", "Use Single Sign On to continue": "Ga verder met eenmalige aanmelding", "Confirm adding this email address by using Single Sign On to prove your identity.": "Bevestig je identiteit met je eenmalige aanmelding om dit e-mailadres toe te voegen.", @@ -1989,7 +1989,7 @@ "Command failed": "Opdracht mislukt", "Could not find user in room": "Kon die deelnemer aan het gesprek niet vinden", "Please supply a widget URL or embed code": "Gelieve een widgetURL of in te bedden code te geven", - "Send a bug report with logs": "Rapporteer een fout, met foutopsporingslogboek bijgesloten", + "Send a bug report with logs": "Stuur een bugrapport met logs", "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s heeft het gesprek %(oldRoomName)s hernoemd tot %(newRoomName)s.", "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s heeft dit gesprek de nevenadressen %(addresses)s toegekend.", "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s heeft dit gesprek het nevenadres %(addresses)s toegekend.", @@ -2315,7 +2315,7 @@ "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Uw homeserver wees uw inlogpoging af. Dit kan zijn doordat het te lang heeft geduurd. Probeer het opnieuw. Als dit probleem zich blijft voordoen, neem contact op met de beheerder van uw homeserver.", "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Uw homeserver was onbereikbaar en kon u niet inloggen, probeer het opnieuw. Wanneer dit probleem zich blijft voordoen, neem contact op met de beheerder van uw homeserver.", "Try again": "Probeer opnieuw", - "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "De browser is verzocht uw homeserver te onthouden die u gebruikt om zich aan te melden, maar is deze vergeten. Ga naar de aanmeldpagina en probeer het opnieuw.", + "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "De browser is verzocht uw homeserver te onthouden die u gebruikt om in te loggen, maar helaas heeft de browser deze vergeten. Ga naar de inlog-pagina en probeer het opnieuw.", "We couldn't log you in": "We konden u niet inloggen", "Room Info": "Gespreksinfo", "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is de grootste openbare homeserver van de wereld, dus het is een goede plek voor vele.", @@ -2366,7 +2366,7 @@ "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "De beheerder van uw server heeft eind-tot-eind-versleuteling standaard uitgeschakeld in alle privégesprekken en directe gesprekken.", "Scroll to most recent messages": "Spring naar meest recente bericht", "The authenticity of this encrypted message can't be guaranteed on this device.": "De echtheid van dit versleutelde bericht kan op dit apparaat niet worden gegarandeerd.", - "To link to this room, please add an address.": "Voeg een adres toe om naar deze kamer te verwijzen.", + "To link to this room, please add an address.": "Voeg een adres toe om naar dit gesprek te kunnen verwijzen.", "Remove messages sent by others": "Berichten van anderen verwijderen", "Privacy": "Privacy", "Keyboard Shortcuts": "Sneltoetsen", @@ -2416,8 +2416,8 @@ "sends snowfall": "Stuur sneeuwvlokken", "sends confetti": "verstuurt confetti", "sends fireworks": "Stuur vuurwerk", - "Downloading logs": "Logboeken downloaden", - "Uploading logs": "Logboeken versturen", + "Downloading logs": "Logs downloaden", + "Uploading logs": "Logs uploaden", "Use Ctrl + Enter to send a message": "Gebruik Ctrl + Enter om een bericht te sturen", "Use Command + Enter to send a message": "Gebruik Command (⌘) + Enter om een bericht te sturen", "Use Ctrl + F to search": "Ctrl + F om te zoeken gebruiken", @@ -2425,7 +2425,7 @@ "Use a more compact ‘Modern’ layout": "Compacte 'Modern'-layout inschakelen", "Use custom size": "Aangepaste lettergrootte gebruiken", "Font size": "Lettergrootte", - "Enable advanced debugging for the room list": "Geavanceerde foutopsporing voor de gesprekkenlijst inschakelen", + "Enable advanced debugging for the room list": "Geavanceerde bugopsporing voor de gesprekkenlijst inschakelen", "Render LaTeX maths in messages": "Weergeef LaTeX-wiskundenotatie in berichten", "Change notification settings": "Meldingsinstellingen wijzigen", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", @@ -2449,7 +2449,7 @@ "%(senderName)s has updated the widget layout": "%(senderName)s heeft de widget-indeling bijgewerkt", "%(senderName)s declined the call.": "%(senderName)s heeft de oproep afgewezen.", "(an error occurred)": "(een fout is opgetreden)", - "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "U heeft eerder een nieuwere versie van %(brand)s in deze sessie gebruikt. Om deze versie opnieuw met eind-tot-eind-versleuteling te gebruiken, zult u zich moeten afmelden en opnieuw aanmelden.", + "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "U heeft eerder een nieuwere versie van %(brand)s in deze sessie gebruikt. Om deze versie opnieuw met eind-tot-eind-versleuteling te gebruiken, zult u moeten uitloggen en opnieuw inloggen.", "Block anyone not part of %(serverName)s from ever joining this room.": "Weiger iedereen die geen deel uitmaakt van %(serverName)s aan dit gesprek deel te nemen.", "Create a room in %(communityName)s": "Een gesprek aanmaken in %(communityName)s", "Enable end-to-end encryption": "Eind-tot-eind-versleuteling inschakelen", @@ -2467,7 +2467,7 @@ "Show": "Toon", "People you know on %(brand)s": "Personen die u kent van %(brand)s", "Add another email": "Nog een e-mailadres toevoegen", - "Download logs": "Download logboeken", + "Download logs": "Logs downloaden", "Add a new server...": "Een nieuwe server toevoegen…", "Server name": "Servernaam", "Add a new server": "Een nieuwe server toevoegen", @@ -2479,8 +2479,8 @@ "Enter a server name": "Geef een servernaam", "Continue with %(provider)s": "Doorgaan met %(provider)s", "Homeserver": "Homeserver", - "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "U kunt de aangepaste serverinstellingen gebruiken om u aan te melden bij andere Matrix-servers, door een andere homeserver-URL in te voeren. Dit laat u toe Element te gebruiken met een bestaande Matrix-account bij een andere homeserver.", - "Server Options": "Serverinstellingen", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "U kunt de server opties wijzigen om in te loggen bij andere Matrix-servers, wijzig hiervoor de homeserver-URL. Hiermee kunt u Element gebruiken met een al bestaand Matrix-account van een andere homeserver.", + "Server Options": "Server opties", "This address is already in use": "Dit adres is al in gebruik", "This address is available to use": "Dit adres kan worden gebruikt", "Please provide a room address": "Geef een gespreksadres", @@ -2539,7 +2539,7 @@ "Invite by email": "Via e-mail uitnodigen", "Click the button below to confirm your identity.": "Druk op de knop hieronder om uw identiteit te bevestigen.", "Confirm to continue": "Bevestig om door te gaan", - "Report a bug": "Een fout rapporteren", + "Report a bug": "Een bug rapporteren", "Comment": "Opmerking", "Add comment": "Opmerking toevoegen", "Tell us below how you feel about %(brand)s so far.": "Vertel ons hoe %(brand)s u tot dusver bevalt.", @@ -2640,7 +2640,7 @@ "Use this when referencing your community to others. The community ID cannot be changed.": "Gebruik dit om anderen naar uw gemeenschap te verwijzen. De gemeenschaps-ID kan later niet meer veranderd worden.", "Please go into as much detail as you like, so we can track down the problem.": "Gebruik a.u.b. zoveel mogelijk details, zodat wij uw probleem kunnen vinden.", "There are two ways you can provide feedback and help us improve %(brand)s.": "U kunt op twee manieren feedback geven en ons helpen %(brand)s te verbeteren.", - "Please view existing bugs on Github first. No match? Start a new one.": "Bekijk eerst de bestaande problemen op Github. Maak een nieuwe aan wanneer u uw probleem niet heeft gevonden.", + "Please view existing bugs on Github first. No match? Start a new one.": "Bekijk eerst de bestaande bugs op GitHub. Maak een nieuwe aan wanneer u uw bugs niet heeft gevonden.", "Invite someone using their name, email address, username (like ) or share this room.": "Nodig iemand uit door gebruik te maken van hun naam, e-mailadres, gebruikersnaam (zoals ) of deel dit gesprek.", "Invite someone using their name, username (like ) or share this room.": "Nodig iemand uit door gebruik te maken van hun naam, gebruikersnaam (zoals ) of deel dit gesprek.", "Send feedback": "Feedback versturen", @@ -2738,13 +2738,13 @@ "Use Security Key or Phrase": "Gebruik veiligheidssleutel of -wachtwoord", "Decide where your account is hosted": "Kies waar uw account wordt gehost", "Host account on": "Host uw account op", - "Already have an account? Sign in here": "Heeft u al een account? Aanmelden", + "Already have an account? Sign in here": "Heeft u al een account? Inloggen", "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s of %(usernamePassword)s", "Continue with %(ssoButtons)s": "Ga verder met %(ssoButtons)s", "That username already exists, please try another.": "Die gebruikersnaam bestaat al, probeer een andere.", "New? Create account": "Nieuw? Maak een account aan", "If you've joined lots of rooms, this might take a while": "Als u zich bij veel gesprekken heeft aangesloten, kan dit een tijdje duren", - "Signing In...": "Aanmelden...", + "Signing In...": "Inloggen...", "Syncing...": "Synchroniseren...", "There was a problem communicating with the homeserver, please try again later.": "Er was een communicatieprobleem met de homeserver, probeer het later opnieuw.", "Community and user menu": "Gemeenschaps- en gebruikersmenu", @@ -2754,7 +2754,7 @@ "User settings": "Gebruikersinstellingen", "Security & privacy": "Veiligheid & privacy", "New here? Create an account": "Nieuw hier? Maak een account", - "Got an account? Sign in": "Heeft u een account? Aanmelden", + "Got an account? Sign in": "Heeft u een account? Inloggen", "Failed to find the general chat for this community": "De algemene chat voor deze gemeenschap werd niet gevonden", "Filter rooms and people": "Gespreken en personen filteren", "Explore rooms in %(communityName)s": "Ontdek de gesprekken van %(communityName)s", @@ -2774,8 +2774,8 @@ "Create community": "Gemeenschap aanmaken", "Attach files from chat or just drag and drop them anywhere in a room.": "Voeg bestanden toe vanuit het gesprek of sleep ze in een gesprek.", "No files visible in this room": "Geen bestanden zichtbaar in dit gesprek", - "Sign in with SSO": "Aanmelden met SSO", - "Use email to optionally be discoverable by existing contacts.": "Gebruik e-mail om optioneel ontdekt te worden door bestaande contacten.", + "Sign in with SSO": "Inloggen met SSO", + "Use email to optionally be discoverable by existing contacts.": "Optioneel kunt u uw e-mail ook gebruiken om ontdekt te worden door al bestaande contacten.", "Use email or phone to optionally be discoverable by existing contacts.": "Gebruik e-mail of telefoon om optioneel ontdekt te kunnen worden door bestaande contacten.", "Add an email to be able to reset your password.": "Voeg een e-mail toe om uw wachtwoord te kunnen resetten.", "Forgot password?": "Wachtwoord vergeten?", @@ -2852,7 +2852,7 @@ "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Door tijdelijk door te gaan, krijgt het installatieproces van %(hostSignupBrand)s toegang tot uw account om geverifieerde e-mailadressen op te halen. Deze gegevens worden niet opgeslagen.", "Failed to connect to your homeserver. Please close this dialog and try again.": "Kan geen verbinding maken met uw homeserver. Sluit dit dialoogvenster en probeer het opnieuw.", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Weet u zeker dat u het aanmaken van de host wilt afbreken? Het proces kan niet worden voortgezet.", - "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: Als u een bug start, stuur ons dan debug logs om ons te helpen het probleem op te sporen.", + "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: Als u een nieuwe bug maakt, stuur ons dan uw foutenlogboek om ons te helpen het probleem op te sporen.", "There was an error updating your community. The server is unable to process your request.": "Er is een fout opgetreden bij het updaten van uw gemeenschap. De server is niet in staat om uw verzoek te verwerken.", "There was an error finding this widget.": "Er is een fout opgetreden bij het vinden van deze widget.", "Server did not return valid authentication information.": "Server heeft geen geldige verificatiegegevens teruggestuurd.", @@ -2889,7 +2889,7 @@ "Submit logs": "Logs versturen", "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Berichten in dit gesprek zijn eind-tot-eind-versleuteld. Als personen deelnemen, kan u ze verifiëren in hun profiel, tik hiervoor op hun avatar.", "In encrypted rooms, verify all users to ensure it’s secure.": "Controleer alle gebruikers in versleutelde gesprekken om er zeker van te zijn dat het veilig is.", - "Verify all users in a room to ensure it's secure.": "Controleer alle gebruikers in een gesprek om er zeker van te zijn dat hij veilig is.", + "Verify all users in a room to ensure it's secure.": "Controleer alle gebruikers in een gesprek om er zeker van te zijn dat het veilig is.", "%(count)s people|one": "%(count)s persoon", "Add widgets, bridges & bots": "Widgets, bruggen & bots toevoegen", "Edit widgets, bridges & bots": "Widgets, bruggen & bots bewerken", @@ -2921,10 +2921,10 @@ "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s kan versleutelde berichten niet veilig lokaal opslaan in een webbrowser. Gebruik %(brand)s Desktop om versleutelde berichten in zoekresultaten te laten verschijnen.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Veilig lokaal opslaan van versleutelde berichten zodat ze in de zoekresultaten verschijnen, gebruik %(size)s voor het opslaan van berichten uit %(rooms)s gesprek.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Veilig lokaal opslaan van versleutelde berichten zodat ze in de zoekresultaten verschijnen, gebruik %(size)s voor het opslaan van berichten uit %(rooms)s gesprekken.", - "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Elke sessie die door een gebruiker wordt gebruikt, afzonderlijk verifiëren om deze als vertrouwd aan te merken, waarbij geen vertrouwen wordt gesteld in kruiselings ondertekende apparaten.", - "User signing private key:": "Gebruiker ondertekening privésleutel:", - "Master private key:": "Hoofd privésleutel:", - "Self signing private key:": "Zelfondertekenende privésleutel:", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Verifieer elke sessie die door een gebruiker wordt gebruikt afzonderlijk. Dit markeert hem als vertrouwd zonder te vertrouwen op kruislings ondertekende apparaten.", + "User signing private key:": "Gebruikerondertekening-privésleutel:", + "Master private key:": "Hoofdprivésleutel:", + "Self signing private key:": "Zelfondertekening-privésleutel:", "Cross-signing is not set up.": "Kruiselings ondertekenen is niet ingesteld.", "Cross-signing is ready for use.": "Kruiselings ondertekenen is klaar voor gebruik.", "Your server isn't responding to some requests.": "Uw server reageert niet op sommige verzoeken.", @@ -2941,13 +2941,13 @@ "Minimize dialog": "Dialoog minimaliseren", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Als u nu annuleert, kunt u versleutelde berichten en gegevens verliezen als u geen toegang meer heeft tot uw login.", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Bevestig uw identiteit door deze login te verifiëren vanuit een van uw andere sessies, waardoor u toegang krijgt tot uw versleutelde berichten.", - "Verify this login": "Controleer deze login", + "Verify this login": "Deze inlog verifiëren", "To continue, use Single Sign On to prove your identity.": "Om verder te gaan, gebruik uw eenmalige aanmelding om uw identiteit te bewijzen.", "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Plakt ( ͡° ͜ʖ ͡°) vóór een bericht zonder opmaak", "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Plakt ┬──┬ ノ( ゜-゜ノ) vóór een bericht zonder opmaak", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Plakt (╯°□°)╯︵ ┻━┻ vóór een bericht zonder opmaak", "Liberate your communication": "Bevrijd uw communicatie", - "Create a Group Chat": "Maak een groepsgesprek aan", + "Create a Group Chat": "Maak een groepsgesprek", "Send a Direct Message": "Start een direct gesprek", "Welcome to %(appName)s": "Welkom bij %(appName)s", "Add a topic to help people know what it is about.": "Stel een gespreksonderwerp in zodat de personen weten waar het over gaat.", @@ -3024,7 +3024,7 @@ "Start audio stream": "Audio-stream starten", "Failed to start livestream": "Starten van livestream is mislukt", "Unable to start audio streaming.": "Kan audio-streaming niet starten.", - "Save Changes": "Wijzigingen Opslaan", + "Save Changes": "Wijzigingen opslaan", "Saving...": "Opslaan...", "View dev tools": "Bekijk dev tools", "Leave Space": "Space verlaten", @@ -3136,7 +3136,7 @@ "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Een nieuwe login heeft toegang tot uw account: %(name)s (%(deviceID)s) op %(ip)s", "You have unverified logins": "U heeft ongeverifieerde logins", "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Zonder verifiëren heeft u geen toegang tot al uw berichten en kan u als onvertrouwd aangemerkt staan bij anderen.", - "Verify your identity to access encrypted messages and prove your identity to others.": "Verifeer uw identiteit om toegang te krijgen tot uw versleutelde berichten en uw identiteit te bewijzen voor anderen.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Verifeer uw identiteit om toegang te krijgen tot uw versleutelde berichten en om uw identiteit te bewijzen voor anderen.", "Use another login": "Gebruik andere login", "Please choose a strong password": "Kies een sterk wachtwoord", "You can add more later too, including already existing ones.": "U kunt er later nog meer toevoegen, inclusief al bestaande gesprekken.", @@ -3170,7 +3170,7 @@ "Share decryption keys for room history when inviting users": "Deel ontsleutelsleutels voor de gespreksgeschiedenis wanneer u personen uitnodigd", "Send and receive voice messages (in development)": "Verstuur en ontvang audioberichten (in ontwikkeling)", "%(deviceId)s from %(ip)s": "%(deviceId)s van %(ip)s", - "Review to ensure your account is safe": "Controleer om u te verzekeren dat uw account veilig is", + "Review to ensure your account is safe": "Controleer ze zodat uw account veilig is", "Sends the given message as a spoiler": "Verstuurt het bericht als een spoiler", "You are the only person here. If you leave, no one will be able to join in the future, including you.": "U bent de enige persoon hier. Als u weggaat, zal niemand in de toekomst kunnen toetreden, u ook niet.", "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Als u alles reset, zult u opnieuw opstarten zonder vertrouwde sessies, zonder vertrouwde gebruikers, en zult u misschien geen vroegere berichten meer kunnen zien.", @@ -3192,7 +3192,74 @@ "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s leden inclusief %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Inclusief %(commaSeparatedMembers)s", - "View all %(count)s members|one": "Bekijk 1 lid", + "View all %(count)s members|one": "1 lid bekijken", "View all %(count)s members|other": "Bekijk alle %(count)s leden", - "Failed to send": "Verzenden is mislukt" + "Failed to send": "Verzenden 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 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?", + "Filter all spaces": "Alle spaces filteren", + "Delete recording": "Opname verwijderen", + "Stop the recording": "Opname stoppen", + "%(count)s results in all spaces|one": "%(count)s resultaat in alle spaces", + "%(count)s results in all spaces|other": "%(count)s resultaten in alle spaces", + "You have no ignored users.": "U heeft geen gebruiker genegeerd.", + "Play": "Afspelen", + "Pause": "Pauze", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Dit is een experimentele functie. Voorlopig moeten nieuwe personen die een uitnodiging krijgen de gebruiken om daadwerkelijk deel te nemen.", + "To join %(spaceName)s, turn on the Spaces beta": "Om aan %(spaceName)s deel te nemen moet u de Spaces beta inschakelen", + "To view %(spaceName)s, turn on the Spaces beta": "Om %(spaceName)s te bekijken moet u de Spaces beta inschakelen", + "Select a room below first": "Start met selecteren van een gesprek hieronder", + "Communities are changing to Spaces": "Gemeenschappen worden vervangen door Spaces", + "Join the beta": "Beta inschakelen", + "Leave the beta": "Beta verlaten", + "Beta": "Beta", + "Tap for more info": "Klik voor meer info", + "Spaces is a beta feature": "Spaces zijn in beta", + "Want to add a new room instead?": "Wilt u anders een nieuw gesprek toevoegen?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Gesprek toevoegen...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Gesprekken toevoegen... (%(progress)s van %(count)s)", + "Not all selected were added": "Niet alle geselecteerden zijn toegevoegd", + "You can add existing spaces to a space.": "U kunt bestaande spaces toevoegen aan een space.", + "Feeling experimental?": "Zin in een experiment?", + "You are not allowed to view this server's rooms list": "U heeft geen toegang tot deze server zijn gesprekkenlijst", + "Error processing voice message": "Fout bij verwerking spraakbericht", + "We didn't find a microphone on your device. Please check your settings and try again.": "We hebben geen microfoon gevonden op uw apparaat. Controleer uw instellingen en probeer het opnieuw.", + "No microphone found": "Geen microfoon gevonden", + "We were unable to access your microphone. Please check your browser settings and try again.": "We hebben geen toegang tot uw microfoon. Controleer uw browserinstellingen en probeer het opnieuw.", + "Unable to access your microphone": "Geen toegang tot uw microfoon", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Zin in een experiment? Labs is de beste manier om dingen vroeg te krijgen, nieuwe functies uit te testen en ze te helpen vormen voordat ze daadwerkelijk worden gelanceerd. Lees meer.", + "Your access token gives full access to your account. Do not share it with anyone.": "Uw toegangstoken geeft u toegang to uw account. Deel hem niet met anderen.", + "Access Token": "Toegangstoken", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces zijn de nieuwe manier om gesprekken en personen te groeperen. Om aan een bestaande space deel te nemen heeft u een uitnodiging nodig.", + "Please enter a name for the space": "Vul een naam in voor deze space", + "Connecting": "Verbinden", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Peer-to-peer voor 1op1 oproepen toestaan (als u dit inschakelt kunnen andere personen mogelijk uw ipadres zien)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "De beta is beschikbaar voor web, desktop en Android. Sommige functies zijn nog niet beschikbaar op uw homeserver.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "U kunt de beta elk moment verlaten via instellingen of door op de beta badge hierboven te klikken.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s zal herladen met Spaces ingeschakeld. Gemeenschappen en labels worden verborgen.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "De beta is beschikbaar voor web, desktop en Android. Bedankt dat u de beta wilt proberen.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s zal herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden.", + "Spaces are a new way to group rooms and people.": "Spaces zijn de nieuwe manier om gesprekken en personen te groeperen.", + "Message search initialisation failed": "Zoeken in berichten opstarten is mislukt", + "Spaces are a beta feature.": "Spaces zijn een beta functie.", + "Search names and descriptions": "Namen en beschrijvingen zoeken", + "You may contact me if you have any follow up questions": "U mag contact met mij opnemen als u nog vervolg vragen heeft", + "To leave the beta, visit your settings.": "Om de beta te verlaten, ga naar uw instellingen.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Uw platform en gebruikersnaam zullen worden opgeslagen om onze te helpen uw feedback zo goed mogelijk te gebruiken.", + "%(featureName)s beta feedback": "%(featureName)s beta feedback", + "Thank you for your feedback, we really appreciate it.": "Bedankt voor uw feedback, we waarderen het enorm.", + "Beta feedback": "Beta feedback", + "Add reaction": "Reactie toevoegen", + "Send and receive voice messages": "Stuur en ontvang spraakberichten", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Uw feedback maakt spaces beter. Hoe meer details u kan geven, des te beter.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Als u de pagina nu verlaat zal %(brand)s herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden.", + "Space Autocomplete": "Space Autocomplete", + "Go to my space": "Ga naar mijn space", + "sends space invaders": "verstuur space invaders", + "Sends the given message with a space themed effect": "Verstuur het bericht met een space-thema-effect", + "See when people join, leave, or are invited to your active room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd in uw actieve gesprek", + "Kick, ban, or invite people to your active room, and make you leave": "Verwijder, verban of nodig personen uit voor uw actieve gesprek en uzelf laten vertrekken", + "See when people join, leave, or are invited to this room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd voor dit gesprek", + "Kick, ban, or invite people to this room, and make you leave": "Verwijder, verban of verwijder personen uit dit gesprek en uzelf laten vertrekken" } diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index ab9a478446..83c6c25833 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -2265,5 +2265,11 @@ "There was an error finding this widget.": "Wystąpił błąd podczas próby odnalezienia tego widżetu.", "Active Widgets": "Aktywne widżety", "Encryption not enabled": "Nie włączono szyfrowania", - "Encryption enabled": "Włączono szyfrowanie" + "Encryption enabled": "Włączono szyfrowanie", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Twój serwer domowy był nieosiągalny i nie mógł Cię zalogować. Spróbuj ponownie. Jeśli to się powtórzy, skontaktuj się z administratorem swojego serwera.", + "Try again": "Spróbuj ponownie", + "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Poprosiliśmy przeglądarkę o zapamiętanie, z którego serwera głównego korzystasz, aby umożliwić Ci logowanie, ale niestety Twoja przeglądarka o tym zapomniała. Przejdź do strony logowania i spróbuj ponownie.", + "We couldn't log you in": "Nie mogliśmy Cię zalogować", + "You're already in a call with this person.": "Prowadzisz już rozmowę z tą osobą.", + "Already in call": "Już dzwoni" } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 305e7dc610..bd294093f9 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3294,5 +3294,72 @@ "Including %(commaSeparatedMembers)s": "Prfshi %(commaSeparatedMembers)s", "View all %(count)s members|one": "Shihni 1 anëtar", "View all %(count)s members|other": "Shihni krejt %(count)s anëtarët", - "Failed to send": "S’u arrit të dërgohet" + "Failed to send": "S’u arrit të dërgohet", + "Enter your Security Phrase a second time to confirm it.": "Jepni Frazën tuaj të Sigurisë edhe një herë, për ta ripohuar.", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Zgjidhni dhoma ose biseda që të shtohen. Kjo është thjesht një hapësirë për ju, s’do ta dijë kush tjetër. Mund të shtoni të tjerë më vonë.", + "What do you want to organise?": "Ç’doni të sistemoni?", + "Filter all spaces": "Filtro krejt hapësirat", + "Delete recording": "Fshije regjistrimin", + "Stop the recording": "Ndale regjistrimin", + "%(count)s results in all spaces|one": "%(count)s përfundim në krejt hapësirat", + "%(count)s results in all spaces|other": "%(count)s përfundime në krejt hapësirat", + "You have no ignored users.": "S’keni përdorues të shpërfillur.", + "Play": "Luaje", + "Pause": "Ndalesë", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Kjo është një veçori eksperimentale. Hëpërhë, përdoruesve të rinj që marrin një ftesë, do t’u duhet ta hapin ftesën në , që të marrin pjesë.", + "To join %(spaceName)s, turn on the Spaces beta": "Për të hyrë në %(spaceName)s, aktivizoni beta-n për Hapësira", + "To view %(spaceName)s, turn on the Spaces beta": "Për të parë %(spaceName)s, aktivizoni beta-n për Hapësira", + "Spaces are a beta feature.": "Hapësirat janë një veçori në version beta.", + "Search names and descriptions": "Kërko te emra dhe përshkrime", + "Select a room below first": "Së pari, përzgjidhni më poshtë një dhomë", + "Communities are changing to Spaces": "Bashkësitë po ndryshojnë në Hapësira", + "Join the beta": "Merrni pjesë te beta", + "Leave the beta": "Braktiseni beta-n", + "Beta": "Beta", + "Tap for more info": "Për më tepër hollësi, prekeni", + "Spaces is a beta feature": "Hapësirat janë një veçori në version beta", + "You may contact me if you have any follow up questions": "Mund të lidheni me mua, nëse keni pyetje të mëtejshme", + "To leave the beta, visit your settings.": "Që të braktisni beta-n, vizitoni rregullimet tuaja.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Platforma dhe emri juaj i përdoruesit do të mbahen shënim, për të na ndihmuar t’i përdorim përshtypjet tuaja sa më shumë që të mundemi.", + "%(featureName)s beta feedback": "Përshtypje për beta %(featureName)s", + "Thank you for your feedback, we really appreciate it.": "Faleminderit për përshtypjet tuaja, vërtet e çmojmë.", + "Beta feedback": "Përshtypje për versionin Beta", + "Want to add a new room instead?": "Doni të shtohet një dhomë e re, në vend të kësaj?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Po shtohet dhomë…", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Po shtohen dhoma… (%(progress)s nga %(count)s)", + "Not all selected were added": "S’u shtuan të gjithë të përzgjedhurit", + "You can add existing spaces to a space.": "Mund të shtoni hapësira ekzistuese te një hapësirë.", + "Feeling experimental?": "Ndiheni eksperimentues?", + "You are not allowed to view this server's rooms list": "S’keni leje të shihni listën e dhomave të këtij shërbyesi", + "Zoom in": "Zmadhoje", + "Zoom out": "Zvogëloje", + "Add reaction": "Shtoni reagim", + "Error processing voice message": "Gabim në përpunimin e mesazhit zanor", + "Accept on your other login…": "Pranojeni te hyrja tjetër e juaja…", + "We didn't find a microphone on your device. Please check your settings and try again.": "S’gjetëm mikrofon në pajisjen tuaj. Ju lutemi, kontrolloni rregullimet tuaja dhe riprovoni.", + "No microphone found": "S’u gjet mikrofon", + "We were unable to access your microphone. Please check your browser settings and try again.": "S’qemë në gjendje të përdorim mikrofonin tuaj. Ju lutemi, kontrolloni rregullimet e shfletuesit tuaj dhe riprovoni.", + "Unable to access your microphone": "S’arrihet të përdoret mikrofoni juaj", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Ndiheni eksperimentues? Laboratorët janë rruga më e mirë për t’u marrë herët me gjërat, për të provuar veçori të reja dhe për të ndihmuar t’u jepet formë atyre, përpara se të hidhen faktikisht në qarkullim. Mësoni më tepër.", + "Your access token gives full access to your account. Do not share it with anyone.": "Tokeni-i juaj i hyrjeve jep hyrje të plotë në llogarinë tuaj. Mos ia jepni kujt.", + "Access Token": "Token Hyrjesh", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Hapësirat janë rrugë e re për të grupuar dhoma dhe njerëz. Për t’u bërë pjesë e një hapësire ekzistuese, do t’ju duhet një ftesë.", + "Please enter a name for the space": "Ju lutemi, jepni një emër për hapësirën", + "Connecting": "Po lidhet", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Lejo Tek-për-Tek për thirrje 1:1 (nëse e aktivizoni këtë, pala tjetër mund të jetë në gjendje të shohë adresën tuaj IP)", + "New spinner design": "Rrotullues i ri", + "Send and receive voice messages": "Dërgoni dhe merrni mesazhe zanorë", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Përshtypjet tuaja do t’i bëjnë hapësirat më të mira. Sa më shumë hollësi që të jepni, aq më mirë.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta e gatshme për web, desktop dhe Android. Disa veçori mund të mos jenë të përdorshme në shërbyesin tuaj Home.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Beta-n mund ta braktisni në çfarëdo kohe, që nga rregullimet, ose duke prekur një stemë beta, si ajo më sipër.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s do të ringarkohet me Hapësirat të aktivizuara. Bashkësitë dhe etiketat vetjake do të jenë të fshehura.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta e gatshme për web, desktop dhe Android. Faleminderit që provoni beta-n.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Nëse ikni, %(brand)s-i do të ringarkohet me Hapësira të çaktivizuara. Bashkësitë dhe etiketat vetjake do të jenë sërish të dukshme.", + "Spaces are a new way to group rooms and people.": "Hapësirat janë një rrugë e re për të grupuar dhoma dhe njerëz.", + "Message search initialisation failed": "Dështoi gatitje kërkimi mesazhesh", + "Go to my space": "Kalo te hapësira ime", + "sends space invaders": "dërgon pushtues hapësire", + "Sends the given message with a space themed effect": "E dërgon mesazhin e dhënë me një efekt teme hapësinore", + "See when people join, leave, or are invited to your active room": "Shihni kur persona vijnë, ikin ose janë ftuar në dhomën tuaj aktive", + "See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë" } diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 64cd8ae6ac..a50c039e9e 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3239,5 +3239,71 @@ "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s medlemmar inklusive %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Inklusive %(commaSeparatedMembers)s", - "Failed to send": "Misslyckades att skicka" + "Failed to send": "Misslyckades att skicka", + "Enter your Security Phrase a second time to confirm it.": "Ange din säkerhetsfras igen för att bekräfta den.", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Välj rum eller konversationer att lägga till. Detta är bara ett utrymmer för dig, ingen kommer att informeras. Du kan lägga till fler senare.", + "What do you want to organise?": "Vad vill du organisera?", + "Filter all spaces": "Filtrera alla utrymmen", + "Delete recording": "Radera inspelningen", + "Stop the recording": "Stoppa inspelningen", + "%(count)s results in all spaces|one": "%(count)s resultat i alla utrymmen", + "%(count)s results in all spaces|other": "%(count)s resultat i alla utrymmen", + "You have no ignored users.": "Du har inga ignorerade användare.", + "Play": "Spela", + "Pause": "Pausa", + "Message search initialisation failed": "Initialisering av meddelandesökning misslyckades", + "To view %(spaceName)s, turn on the Spaces beta": "För att se %(spaceName)s, aktivera utrymmesbetan", + "Spaces are a beta feature.": "Utrymmen är en betafunktion.", + "Search names and descriptions": "Sök namn och beskrivningar", + "Select a room below first": "Välj ett rum nedan först", + "Communities are changing to Spaces": "Gemenskaper byts ut mot utrymmen", + "Join the beta": "Gå med i betan", + "Leave the beta": "Lämna betan", + "Beta": "Beta", + "Tap for more info": "Klicka för mer info", + "Spaces is a beta feature": "Utrymmen är en betafunktion", + "You may contact me if you have any follow up questions": "Ni kan kontakta mig om ni har vidare frågor", + "To leave the beta, visit your settings.": "För att lämna betan, besök dina inställningar.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Din plattform och ditt användarnamn kommer att noteras för att hjälpa oss att använda din återkoppling så mycket vi kan.", + "%(featureName)s beta feedback": "%(featureName)s betaåterkoppling", + "Thank you for your feedback, we really appreciate it.": "Tack för din återkoppling, vi uppskattar det verkligen.", + "Beta feedback": "Betaåterkoppling", + "Want to add a new room instead?": "Vill du lägga till ett nytt rum istället?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Lägger till rum…", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Lägger till rum… (%(progress)s av %(count)s)", + "Not all selected were added": "Inte alla valda tillades", + "You can add existing spaces to a space.": "Du kan lägga till existerande utrymmen till ett utrymme.", + "Feeling experimental?": "Känner du dig äventyrlig?", + "You are not allowed to view this server's rooms list": "Du tillåts inte att se den här serverns rumslista", + "Add reaction": "Lägg till reaktion", + "Error processing voice message": "Fel vid hantering av röstmeddelande", + "We didn't find a microphone on your device. Please check your settings and try again.": "Vi kunde inte hitta en mikrofon på din enhet. Vänligen kolla dina inställningar och försök igen.", + "No microphone found": "Ingen mikrofon hittad", + "We were unable to access your microphone. Please check your browser settings and try again.": "Vi kunde inte komma åt din mikrofon. Vänligen kolla dina webbläsarinställningar och försök igen.", + "Unable to access your microphone": "Kan inte komma åt din mikrofon", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Känner du dig äventyrlig? Experiment är det bästa sättet att få saker tidigt, testa nya funktioner och hjälpa till att forma dem innan de egentligen släpps Läs mer.", + "Your access token gives full access to your account. Do not share it with anyone.": "Din åtkomsttoken ger full åtkomst till ditt konto. Dela den inte med någon.", + "Access Token": "Åtkomsttoken", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Utrymmen är ett nytt sätt att gruppera rum och personer. För att gå med i existerande utrymme så behöver du en inbjudan.", + "Please enter a name for the space": "Vänligen ange ett namn för utrymmet", + "Connecting": "Ansluter", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Tillåt peer-to-peer för 1:1-samtal (om du aktiverar det hör så kan den andra parten kanske se din IP-adress)", + "Send and receive voice messages": "Skicka och ta emot röstmeddelanden", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Din återkoppling kommer att hjälpa till att göra utrymmen bättre. Ju fler detaljer du kan ge desto bättre.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta tillgänglig för webben, skrivbord och Android. Vissa funktioner kan vara otillgängliga på din hemserver.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Du kan lämna betan när som helst från inställningarna eller genom att trycka en betabricka, som den ovan.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s kommer att ladda om med utrymmen aktiverade. Gemenskaper och anpassade taggar kommer att döljas.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta tillgänglig för webben, skrivbord och Android. Tack för att du provar betan.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Om du lämnar så kommer %(brand)s att ladda om med utrymmen inaktiverade. Gemenskaper och anpassade taggar kommer att synas igen.", + "Spaces are a new way to group rooms and people.": "Utrymmen är nya sätt att gruppera rum och personer.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Det här är en experimentell funktion. För tillfället så behöver nya inbjudna användare öppna inbjudan på för att faktiskt gå med.", + "To join %(spaceName)s, turn on the Spaces beta": "För att gå med i %(spaceName)s, aktivera utrymmesbetan", + "Space Autocomplete": "Utrymmesautokomplettering", + "Go to my space": "Gå till mitt utrymme", + "sends space invaders": "skickar Space Invaders", + "Sends the given message with a space themed effect": "Skickar det givna meddelandet med en effekt med rymdtema", + "See when people join, leave, or are invited to your active room": "Se när folk går med, lämnar eller bjuds in till ditt aktiva rum", + "Kick, ban, or invite people to your active room, and make you leave": "Kicka, banna eller bjuda in folk till ditt aktiva rum, och tvinga dig att lämna", + "See when people join, leave, or are invited to this room": "Se när folk går med, lämnar eller bjuds in till det här rummet", + "Kick, ban, or invite people to this room, and make you leave": "Kicka, banna eller bjuda in folk till det här rummet, och tvinga dig att lämna" } diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 6afe74dbee..af02a40587 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -51,7 +51,7 @@ "Invalid Email Address": "邮箱地址格式错误", "Invalid file%(extra)s": "无效文件%(extra)s", "Return to login screen": "返回登录页面", - "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s 没有通知发送权限 - 请检查您的浏览器设置", + "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s 没有通知发送权限 - 请检查你的浏览器设置", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s 没有通知发送权限 - 请重试", "%(brand)s version:": "%(brand)s 版本:", "Room %(roomId)s not visible": "聊天室 %(roomId)s 已隐藏", @@ -65,7 +65,7 @@ "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s 向 %(targetDisplayName)s 发了加入聊天室的邀请。", "Server error": "服务器错误", "Server may be unavailable, overloaded, or search timed out :(": "服务器可能不可用、超载,或者搜索超时 :(", - "Server may be unavailable, overloaded, or you hit a bug.": "当前服务器可能处于不可用或过载状态,或者您遇到了一个 bug。", + "Server may be unavailable, overloaded, or you hit a bug.": "当前服务器可能处于不可用或过载状态,或者你遇到了一个 bug。", "Server unavailable, overloaded, or something else went wrong.": "服务器可能不可用、超载,或者其他东西出错了.", "Session ID": "会话 ID", "%(senderName)s set a profile picture.": "%(senderName)s 设置了头像。", @@ -272,7 +272,7 @@ "Upload file": "上传文件", "Usage": "用法", "Who can read history?": "谁可以阅读历史消息?", - "You are not in this room.": "您不在此聊天室中。", + "You are not in this room.": "你不在此聊天室中。", "You have no visible notifications": "没有可见的通知", "Not a valid %(brand)s keyfile": "不是有效的 %(brand)s 密钥文件", "%(targetName)s accepted an invitation.": "%(targetName)s 已接受邀请。", @@ -310,11 +310,11 @@ "(no answer)": "(无回复)", "Who can access this room?": "谁有权访问此聊天室?", "You are already in a call.": "您正在通话。", - "You do not have permission to do that in this room.": "您没有进行此操作的权限。", + "You do not have permission to do that in this room.": "你没有进行此操作的权限。", "You cannot place VoIP calls in this browser.": "无法在此浏览器中发起 VoIP 通话。", - "You do not have permission to post to this room": "您没有在此聊天室发送消息的权限", - "You seem to be in a call, are you sure you want to quit?": "您似乎正在进行通话,确定要退出吗?", - "You seem to be uploading files, are you sure you want to quit?": "您似乎正在上传文件,确定要退出吗?", + "You do not have permission to post to this room": "你没有在此聊天室发送消息的权限", + "You seem to be in a call, are you sure you want to quit?": "你似乎正在进行通话,确定要退出吗?", + "You seem to be uploading files, are you sure you want to quit?": "你似乎正在上传文件,确定要退出吗?", "Upload an avatar:": "上传头像:", "An error occurred: %(error_string)s": "发生了一个错误: %(error_string)s", "There are no visible files in this room": "此聊天室中没有可见的文件", @@ -327,13 +327,13 @@ "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s 移除了聊天室头像。", "Something went wrong!": "出了点问题!", "If you already have a Matrix account you can log in instead.": "若您已经拥有 Matrix 帐号,您也可以 登录。", - "Do you want to set an email address?": "您想要设置一个邮箱地址吗?", + "Do you want to set an email address?": "你想要设置一个邮箱地址吗?", "Upload new:": "上传新的:", "Username invalid: %(errMessage)s": "用户名无效: %(errMessage)s", "Verification Pending": "验证等待中", "(unknown failure: %(reason)s)": "(未知错误:%(reason)s)", "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s 收回了 %(targetName)s 的邀请。", - "You cannot place a call with yourself.": "您无法向自己发起通话。", + "You cannot place a call with yourself.": "你无法向自己发起通话。", "You have disabled URL previews by default.": "你已经默认禁用链接预览。", "You have enabled URL previews by default.": "你已经默认启用链接预览。", "Set a display name:": "设置昵称:", @@ -385,10 +385,10 @@ "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s 移除了 %(widgetName)s 挂件", "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s 修改了 %(widgetName)s 挂件", "Unpin Message": "取消置顶消息", - "Add rooms to this community": "添加聊天室到此社区", + "Add rooms to this community": "添加聊天室到此社群", "Call Failed": "呼叫失败", - "Invite new community members": "邀请新社区成员", - "Invite to Community": "邀请到社区", + "Invite new community members": "邀请新社群成员", + "Invite to Community": "邀请到社群", "Ignored user": "已忽略的用户", "You are now ignoring %(userId)s": "你忽略了 %(userId)s", "Unignored user": "未忽略的用户", @@ -450,28 +450,28 @@ "collapse": "折叠", "expand": "展开", "email address": "邮箱地址", - "You have entered an invalid address.": "您输入了无效的地址。", + "You have entered an invalid address.": "你输入了无效的地址。", "Leave": "退出", "Description": "描述", "Warning": "警告", "Room Notification": "聊天室通知", - "The platform you're on": "您使用的平台是", + "The platform you're on": "你使用的平台是", "The version of %(brand)s": "%(brand)s 版本", - "Your language of choice": "您选择的语言是", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "您是否正在使用富文本编辑器的富文本模式", - "Your homeserver's URL": "您的主服务器的链接", + "Your language of choice": "你选择的语言是", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "你是否正在使用富文本编辑器的富文本模式", + "Your homeserver's URL": "你的主服务器的链接", "The information being sent to us to help make %(brand)s better includes:": "正在给我们发送信息以帮助 %(brand)s:", - "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "此页面中含有可用于识别您身份的信息,比如聊天室、用户或群组 ID,这些数据会在发送到服务器前被移除。", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "此页面中含有可用于识别你身份的信息,比如聊天室、用户或群组 ID,这些数据会在发送到服务器前被移除。", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(monthName)s %(day)s %(time)s, %(weekDayName)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(fullYear)s %(monthName)s %(day)s, %(weekDayName)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(fullYear)s %(monthName)s %(day)s %(time)s, %(weekDayName)s", - "Who would you like to add to this community?": "您想把谁添加至此社区中?", - "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "警告:您添加的一切用户都将会对一切知道此社区的 ID 的人公开", - "Which rooms would you like to add to this community?": "您想把哪个聊天室添加至此社区中?", - "Add rooms to the community": "添加聊天室到社区", - "Add to community": "添加到社区", - "Failed to invite users to community": "邀请用户到社区失败", + "Who would you like to add to this community?": "你想把谁添加至此社群中?", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "警告:你添加的一切用户都将会对一切知道此社群的 ID 的人公开", + "Which rooms would you like to add to this community?": "你想把哪个聊天室添加至此社群中?", + "Add rooms to the community": "添加聊天室到社群", + "Add to community": "添加到社群", + "Failed to invite users to community": "邀请用户到社群失败", "Enable inline URL previews by default": "默认启用链接预览", "Disinvite this user?": "是否不再邀请此用户?", "Kick this user?": "是否移除此用户?", @@ -485,24 +485,24 @@ "Members only (since the point in time of selecting this option)": "仅成员(从选中此选项时开始)", "Members only (since they were invited)": "只有成员(从他们被邀请开始)", "Members only (since they joined)": "只有成员(从他们加入开始)", - "Invalid community ID": "无效的社区 ID", - "Create Community": "创建社区", - "Community Name": "社区名称", - "Community ID": "社区 ID", + "Invalid community ID": "无效的社群 ID", + "Create Community": "创建社群", + "Community Name": "社群名称", + "Community ID": "社群 ID", "example": "示例", "Add a Room": "添加聊天室", "Add a User": "添加用户", "Unable to accept invite": "无法接受邀请", "Unable to reject invite": "无法拒绝邀请", - "Leave Community": "退出社区", - "Community Settings": "社区设置", - "Community %(groupId)s not found": "找不到社区 %(groupId)s", - "Your Communities": "我的社区", + "Leave Community": "退出社群", + "Community Settings": "社群设置", + "Community %(groupId)s not found": "找不到社群 %(groupId)s", + "Your Communities": "我的社群", "Failed to set direct chat tag": "无法设定私聊标签", "Failed to remove tag %(tagName)s from room": "移除聊天室标签 %(tagName)s 失败", "Failed to add tag %(tagName)s to room": "无法为聊天室新增标签 %(tagName)s", "Submit debug logs": "提交调试日志", - "Show these rooms to non-members on the community page and room list?": "在社区页面与聊天室列表上对非社区成员显示这些聊天室?", + "Show these rooms to non-members on the community page and room list?": "在社群页面与聊天室列表上对非社群成员显示这些聊天室?", "Failed to invite users to %(groupId)s": "邀请用户到 %(groupId)s 失败", "Failed to invite the following users to %(groupId)s:": "邀请下列用户到 %(groupId)s 失败:", "Failed to add the following rooms to %(groupId)s:": "添加以下聊天室到 %(groupId)s 失败:", @@ -511,10 +511,10 @@ "To use it, just wait for autocomplete results to load and tab through them.": "若要使用自动补全,只要等待自动补全结果加载完成,按 Tab 键切换即可。", "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s 将他们的昵称修改成了 %(displayName)s 。", "Stickerpack": "贴图集", - "You don't currently have any stickerpacks enabled": "您目前没有启用任何贴图集", + "You don't currently have any stickerpacks enabled": "你目前没有启用任何贴图集", "Key request sent.": "已发送密钥共享请求。", - "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "如果您是房间中最后一位有权限的用户,在您降低自己的权限等级后将无法撤回此修改,因为你将无法重新获得权限。", - "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "您将无法撤回此修改,因为您正在将此用户的滥权等级提升至与你相同。", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "如果你是房间中最后一位有权限的用户,在你降低自己的权限等级后将无法撤回此修改,因为你将无法重新获得权限。", + "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "你将无法撤回此修改,因为你正在将此用户的滥权等级提升至与你相同。", "Unmute": "取消静音", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s(滥权等级 %(powerLevelNumber)s)", "Hide Stickers": "隐藏贴图", @@ -528,23 +528,23 @@ "Offline for %(duration)s": "已离线 %(duration)s", "Unknown for %(duration)s": "未知状态已持续 %(duration)s", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s) 在 %(dateTime)s 看到这里", - "'%(groupId)s' is not a valid community ID": "“%(groupId)s” 不是有效的社区 ID", + "'%(groupId)s' is not a valid community ID": "“%(groupId)s” 不是有效的社群 ID", "Flair": "个性徽章", "Code": "代码", - "Remove from community": "从社区中移除", - "Disinvite this user from community?": "是否不再邀请此用户加入本社区?", - "Remove this user from community?": "是否要从社区中移除此用户?", + "Remove from community": "从社群中移除", + "Disinvite this user from community?": "是否不再邀请此用户加入本社群?", + "Remove this user from community?": "是否要从社群中移除此用户?", "Failed to withdraw invitation": "撤回邀请失败", "Failed to remove user from community": "移除用户失败", - "Filter community members": "过滤社区成员", + "Filter community members": "过滤社群成员", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "你确定要从 %(groupId)s 中移除 %(roomName)s 吗?", - "Removing a room from the community will also remove it from the community page.": "从社区中移除房间时,同时也会将其从社区页面中移除。", - "Failed to remove room from community": "从社区中移除聊天室失败", + "Removing a room from the community will also remove it from the community page.": "从社群中移除房间时,同时也会将其从社群页面中移除。", + "Failed to remove room from community": "从社群中移除聊天室失败", "Failed to remove '%(roomName)s' from %(groupId)s": "从 %(groupId)s 中移除 “%(roomName)s” 失败", - "Only visible to community members": "仅对社区成员可见", - "Filter community rooms": "过滤社区聊天室", - "You're not currently a member of any communities.": "您目前不是任何一个社区的成员。", - "Communities": "社区", + "Only visible to community members": "仅对社群成员可见", + "Filter community rooms": "过滤社群聊天室", + "You're not currently a member of any communities.": "你目前不是任何一个社群的成员。", + "Communities": "社群", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s 已加入 %(count)s 次", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s 已加入", @@ -571,13 +571,13 @@ "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)s 撤回了他们的邀请共 %(count)s 次", "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)s 撤回了他们的邀请", "In reply to ": "回复给 ", - "Community IDs cannot be empty.": "社区 ID 不能为空。", - "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "社区 ID 只能包含 a-z、0-9 或 “=_-./” 等字符", - "Something went wrong whilst creating your community": "创建社区时出现问题", - "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "如果您之前使用过较新版本的 %(brand)s,则您的会话可能与当前版本不兼容。请关闭此窗口并使用最新版本。", - "Showing flair for these communities:": "显示这些社区的个性徽章:", - "This room is not showing flair for any communities": "此聊天室没有显示任何社区的个性徽章", - "New community ID (e.g. +foo:%(localDomain)s)": "新社区 ID(例子:+foo:%(localDomain)s)", + "Community IDs cannot be empty.": "社群 ID 不能为空。", + "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "社群 ID 只能包含 a-z、0-9 或 “=_-./” 等字符", + "Something went wrong whilst creating your community": "创建社群时出现问题", + "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "如果你之前使用过较新版本的 %(brand)s,则你的会话可能与当前版本不兼容。请关闭此窗口并使用最新版本。", + "Showing flair for these communities:": "显示这些社群的个性徽章:", + "This room is not showing flair for any communities": "此聊天室没有显示任何社群的个性徽章", + "New community ID (e.g. +foo:%(localDomain)s)": "新社群 ID(例子:+foo:%(localDomain)s)", "URL previews are enabled by default for participants in this room.": "此聊天室默认启用链接预览。", "URL previews are disabled by default for participants in this room.": "此聊天室默认禁用链接预览。", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s 将聊天室的头像更改为 ", @@ -585,56 +585,56 @@ "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix 聊天室 ID", "

    HTML for your community's page

    \n

    \n Use the long description to introduce new members to the community, or distribute\n some important links\n

    \n

    \n You can even use 'img' tags\n

    \n": "", - "Add rooms to the community summary": "将聊天室添加到社区简介中", - "Which rooms would you like to add to this summary?": "您想要将哪个聊天室添加到社区简介?", + "Add rooms to the community summary": "将聊天室添加到社群简介中", + "Which rooms would you like to add to this summary?": "你想要将哪个聊天室添加到社群简介?", "Add to summary": "添加到简介", "Failed to add the following rooms to the summary of %(groupId)s:": "添加以下聊天室到 %(groupId)s 的简介中时失败:", "Failed to remove the room from the summary of %(groupId)s": "从 %(groupId)s 的简介中移除此聊天室时失败", - "The room '%(roomName)s' could not be removed from the summary.": "聊天室 “%(roomName)s” 无法从社区简介中移除。", - "Failed to update community": "更新社区简介失败", - "Unable to leave community": "无法退出社区", + "The room '%(roomName)s' could not be removed from the summary.": "聊天室 “%(roomName)s” 无法从社群简介中移除。", + "Failed to update community": "更新社群简介失败", + "Unable to leave community": "无法退出社群", "Leave %(groupName)s?": "退出 %(groupName)s?", "Featured Rooms:": "核心聊天室:", "Featured Users:": "核心用户:", - "Join this community": "加入此社区", - "%(inviter)s has invited you to join this community": "%(inviter)s 邀请您加入此社区", + "Join this community": "加入此社群", + "%(inviter)s has invited you to join this community": "%(inviter)s 邀请你加入此社群", "Failed to add the following users to the summary of %(groupId)s:": "将下列用户添加至 %(groupId)s 的简介中时失败:", "Failed to remove a user from the summary of %(groupId)s": "从 %(groupId)s 的简介中移除用户时失败", - "You are an administrator of this community": "你是此社区的管理员", - "You are a member of this community": "你是此社区的成员", - "Who can join this community?": "谁可以加入此社区?", + "You are an administrator of this community": "你是此社群的管理员", + "You are a member of this community": "你是此社群的成员", + "Who can join this community?": "谁可以加入此社群?", "Everyone": "所有人", - "Leave this community": "退出此社区", + "Leave this community": "退出此社群", "Long Description (HTML)": "长描述(HTML)", "Old cryptography data detected": "检测到旧的加密数据", - "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "已检测到旧版%(brand)s的数据,这将导致端到端加密在旧版本中发生故障。在此版本中,使用旧版本交换的端对端加密消息可能无法解密。这也可能导致与此版本交换的消息失败。如果您遇到问题,请退出并重新登录。要保留历史消息,请先导出并在重新登录后导入您的密钥。", - "Did you know: you can use communities to filter your %(brand)s experience!": "你知道吗:你可以将社区用作过滤器以增强你的 %(brand)s 使用体验!", - "Create a new community": "创建新社区", - "Error whilst fetching joined communities": "获取已加入社区列表时出现错误", - "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "创建社区,将用户与聊天室整合在一起!搭建自定义社区主页以在 Matrix 宇宙之中标出您的私人空间。", + "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "已检测到旧版%(brand)s的数据,这将导致端到端加密在旧版本中发生故障。在此版本中,使用旧版本交换的端对端加密消息可能无法解密。这也可能导致与此版本交换的消息失败。如果你遇到问题,请退出并重新登录。要保留历史消息,请先导出并在重新登录后导入你的密钥。", + "Did you know: you can use communities to filter your %(brand)s experience!": "你知道吗:你可以将社群用作过滤器以增强你的 %(brand)s 使用体验!", + "Create a new community": "创建新社群", + "Error whilst fetching joined communities": "获取已加入社群列表时出现错误", + "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "创建社群,将用户与聊天室整合在一起!搭建自定义社群主页以在 Matrix 宇宙之中标出你的私人空间。", "%(count)s of your messages have not been sent.|one": "您的消息尚未发送。", "Uploading %(filename)s and %(count)s others|other": "正在上传 %(filename)s 与其他 %(count)s 个文件", "Uploading %(filename)s and %(count)s others|zero": "正在上传 %(filename)s", "Uploading %(filename)s and %(count)s others|one": "正在上传 %(filename)s 与其他 %(count)s 个文件", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "隐私对我们而言重要至极,所以我们不会在分析统计服务中收集任何个人信息或者可用于识别身份的数据。", "Learn more about how we use analytics.": "进一步了解我们如何使用分析统计服务。", - "Please note you are logging into the %(hs)s server, not matrix.org.": "请注意,您正在登录 %(hs)s,而非 matrix.org。", + "Please note you are logging into the %(hs)s server, not matrix.org.": "请注意,你正在登录 %(hs)s,而非 matrix.org。", "This homeserver doesn't offer any login flows which are supported by this client.": "此主服务器不兼容本客户端支持的任何登录方式。", "Opens the Developer Tools dialog": "打开开发者工具窗口", "Notify the whole room": "通知聊天室全体成员", - "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "此操作允许您将加密聊天室中收到的消息的密钥导出为本地文件。您可以将文件导入其他 Matrix 客户端,以便让别的客户端在未收到密钥的情况下解密这些消息。", - "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "导出的文件将允许任何可以读取它的人解密任何他们可以看到的加密消息,因此,您应该小心对待,以确保其安全。为解决此问题,您应当在下面输入密语以加密导出的数据。只有输入相同的密语才能导入数据。", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "此操作允许你将加密聊天室中收到的消息的密钥导出为本地文件。你可以将文件导入其他 Matrix 客户端,以便让别的客户端在未收到密钥的情况下解密这些消息。", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "导出的文件将允许任何可以读取它的人解密任何他们可以看到的加密消息,因此,你应此小心对待,以确保其安全。为解决此问题,你应当在下面输入密语以加密导出的数据。只有输入相同的密语才能导入数据。", "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "导出文件受密语保护。必须输入密语以解密此文件。", - "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "此操作允许您导入之前从另一个 Matrix 客户端中导出的加密密钥文件。导入完成后,您将能够解密那个客户端可以解密的加密消息。", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "此操作允许你导入之前从另一个 Matrix 客户端中导出的加密密钥文件。导入完成后,你将能够解密那个客户端可以解密的加密消息。", "Ignores a user, hiding their messages from you": "忽略用户,隐藏他们发送的消息", "Stops ignoring a user, showing their messages going forward": "解除忽略用户,显示他们的消息", - "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "如果你在 GitHub 提交了一个 bug,调试日志可以帮助我们追踪这个问题。 调试日志包含应用程序使用数据,也就包括您的用户名、您访问的房间或社区的 ID 或别名,以及其他用户的用户名,但不包括聊天记录。", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "如果你在 GitHub 提交了一个 bug,调试日志可以帮助我们追踪这个问题。 调试日志包含应用程序使用数据,也就包括你的用户名、你访问的房间或社群的 ID 或别名,以及其他用户的用户名,但不包括聊天记录。", "Tried to load a specific point in this room's timeline, but was unable to find it.": "尝试加载此聊天室的时间线的特定时间点,但是无法找到。", "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "现在 重新发送消息取消发送 。", "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "現在 重新发送消息取消发送 。你也可以单独选择消息以重新发送或取消。", "Visibility in Room List": "是否在聊天室目录中可见", - "Something went wrong when trying to get your communities.": "获取你加入的社区时发生错误。", - "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "删除挂件时将为聊天室中的所有成员删除。您确定要删除此挂件吗?", + "Something went wrong when trying to get your communities.": "获取你加入的社群时发生错误。", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "删除挂件时将为聊天室中的所有成员删除。你确定要删除此挂件吗?", "Fetching third party location failed": "获取第三方位置失败", "Send Account Data": "发送账号数据", "All notifications are currently disabled for all targets.": "目前所有通知都已禁用。", @@ -647,7 +647,7 @@ "Update": "更新", "What's New": "更新内容", "On": "打开", - "Changelog": "变更日志", + "Changelog": "更改日志", "Waiting for response from server": "正在等待服务器响应", "Send Custom Event": "发送自定义事件", "Advanced notification settings": "通知高级设置", @@ -679,7 +679,7 @@ "Collecting app version information": "正在收集应用版本信息", "Keywords": "关键词", "Enable notifications for this account": "对此账号启用通知", - "Invite to this community": "邀请加入此社区", + "Invite to this community": "邀请加入此社群", "Messages containing keywords": "包含 关键词 的消息", "Room not found": "找不到聊天室", "Tuesday": "星期二", @@ -730,7 +730,7 @@ "Back": "返回", "Reply": "回复", "Show message in desktop notification": "在桌面通知中显示信息", - "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "调试日志包含使用数据(包括您的用户名、您访问过的聊天室/群组的 ID 或别名,以及其他用户的用户名),不含聊天消息。", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "调试日志包含使用数据(包括你的用户名、你访问过的聊天室/群组的 ID 或别名,以及其他用户的用户名),不含聊天消息。", "Unhide Preview": "取消隐藏预览", "Unable to join network": "无法加入网络", "Sorry, your browser is not able to run %(brand)s.": "抱歉,您的浏览器 无法 运行 %(brand)s.", @@ -749,8 +749,8 @@ "Event Type": "事件类型", "Download this file": "下载该文件", "Pin Message": "置顶消息", - "Failed to change settings": "变更设置失败", - "View Community": "查看社区", + "Failed to change settings": "更改设置失败", + "View Community": "查看社群", "Event sent!": "事件已发送!", "View Source": "查看源码", "Event Content": "事件内容", @@ -761,13 +761,13 @@ "You need to be able to invite users to do that.": "你需要有邀请用户的权限才能进行此操作。", "Missing roomId.": "找不到此聊天室 ID 所对应的聊天室。", "e.g. ": "例如:", - "Your device resolution": "您设备的分辨率", + "Your device resolution": "你的设备分辨率", "Always show encryption icons": "总是显示加密标志", - "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "您将被带到一个第三方网站以便验证您的账号来使用 %(integrationsUrl)s 提供的集成。您希望继续吗?", - "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "无法更新聊天室 %(roomName)s 在社区 “%(groupId)s” 中的可见性。", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "你将被带到一个第三方网站以便验证你的账号来使用 %(integrationsUrl)s 提供的集成。你希望继续吗?", + "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "无法更新聊天室 %(roomName)s 在社群 “%(groupId)s” 中的可见性。", "Minimize apps": "最小化应用程序", "Popout widget": "在弹出式窗口中打开挂件", - "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "无法加载被回复的事件,它可能不存在,也可能是您没有权限查看它。", + "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "无法加载被回复的事件,它可能不存在,也可能是你没有权限查看它。", "And %(count)s more...|other": "和 %(count)s 个其他…", "Try using one of the following valid address types: %(validTypesList)s.": "请尝试使用以下的有效邮箱地址格式中的一种:%(validTypesList)s", "e.g. %(exampleValue)s": "例如:%(exampleValue)s", @@ -775,11 +775,11 @@ "A call is already in progress!": "您已在通话中!", "Send analytics data": "发送统计数据", "Enable widget screenshots on supported widgets": "对支持的挂件启用挂件截图", - "Demote yourself?": "是否降低您自己的权限?", + "Demote yourself?": "是否降低你自己的权限?", "Demote": "降权", "A call is currently being placed!": "正在发起通话!", "Permission Required": "需要权限", - "You do not have permission to start a conference call in this room": "您没有在此聊天室发起通话会议的权限", + "You do not have permission to start a conference call in this room": "你没有在此聊天室发起通话会议的权限", "This event could not be displayed": "无法显示此事件", "Share Link to User": "分享链接给其他用户", "Share room": "分享聊天室", @@ -790,53 +790,53 @@ "The email field must not be blank.": "必须输入电子邮箱。", "The phone number field must not be blank.": "必须输入电话号码。", "The password field must not be blank.": "必须输入密码。", - "Display your community flair in rooms configured to show it.": "在启用“显示徽章”的聊天室中显示本社区的个性徽章。", + "Display your community flair in rooms configured to show it.": "在启用“显示徽章”的聊天室中显示本社群的个性徽章。", "Failed to remove widget": "移除小挂件失败", "An error ocurred whilst trying to remove the widget from the room": "尝试从聊天室中移除小部件时发生了错误", - "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "您确定要移除(删除)此事件吗?注意,如果删除了聊天室名称或话题的修改事件,就会撤销此更改。", - "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "这将使您的账号永远不再可用。您将不能登录,或使用相同的用户 ID 重新注册。您的账号将退出所有已加入的聊天室,身份服务器上的账号信息也会被删除。此操作是不可逆的。", - "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "默认情况下,停用您的账号不会忘记您发送的消息 。如果您希望我们忘记您发送的消息,请勾选下面的选择框。", - "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Matrix 中的(历史)信息可见性类似于电子邮件。我们忘记您的消息意味着您发送的消息将不会被发至新注册或未注册的用户,但是已收到您的消息的注册用户依旧可以看到他们的副本。", + "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "你确定要移除(删除)此事件吗?注意,如果删除了聊天室名称或话题的修改事件,就会撤销此更改。", + "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "这将使你的账号永远不再可用。你将不能登录,或使用相同的用户 ID 重新注册。你的账号将退出所有已加入的聊天室,身份服务器上的账号信息也会被删除。此操作是不可逆的。", + "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "默认情况下,停用你的账号不会忘记你发送的消息 。如果你希望我们忘记你发送的消息,请勾选下面的选择框。", + "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Matrix 中的(历史)信息可见性类似于电子邮件。我们忘记你的消息意味着你发送的消息将不会被发至新注册或未注册的用户,但是已收到你的消息的注册用户依旧可以看到他们的副本。", "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "请在停用我的账号的同时忘记我发送的所有消息(警告:这将导致未来的用户看到的对话记录不完整)", - "To continue, please enter your password:": "请输入您的密码以继续:", + "To continue, please enter your password:": "请输入你的密码以继续:", "Clear Storage and Sign Out": "清除数据并退出登录", "Send Logs": "发送日志", "Refresh": "刷新", - "Unable to join community": "无法加入社区", + "Unable to join community": "无法加入社群", "The user '%(displayName)s' could not be removed from the summary.": "无法将用户“%(displayName)s”从简介中移除。", - "Who would you like to add to this summary?": "您想将谁添加到简介中?", - "Add users to the community summary": "添加用户至社区简介", + "Who would you like to add to this summary?": "你想将谁添加到简介中?", + "Add users to the community summary": "添加用户至社群简介", "Collapse Reply Thread": "收起回复", "Share Message": "分享消息", "COPY": "复制", "Share Room Message": "分享聊天室消息", - "Share Community": "分享社区", + "Share Community": "分享社群", "Share User": "分享用户", "Share Room": "分享聊天室", - "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "清除本页储存在您浏览器上的数据或许能修复此问题,但也会导致您退出登录并无法读取任何已加密的聊天记录。", - "We encountered an error trying to restore your previous session.": "我们在尝试恢复您先前的会话时遇到了错误。", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "清除本页储存在你浏览器上的数据或许能修复此问题,但也会导致你退出登录并无法读取任何已加密的聊天记录。", + "We encountered an error trying to restore your previous session.": "我们在尝试恢复你先前的会话时遇到了错误。", "Link to most recent message": "最新消息的链接", "Link to selected message": "选中消息的链接", - "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "至多半个小时内,其他用户可能看不到您社区的 名称头像 的变化。", - "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "这些聊天室对社区成员可见。社区成员可通过点击来加入它们。", - "Your community hasn't got a Long Description, a HTML page to show to community members.
    Click here to open settings and give it one!": "你的社区没有一个很长的描述,一个HTML页面可以展示给社区成员。
    点击这里即可打开设置添加详细介绍!", + "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "至多半个小时内,其他用户可能看不到你社群的 名称头像 的变化。", + "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "这些聊天室对社群成员可见。社群成员可通过点击来加入它们。", + "Your community hasn't got a Long Description, a HTML page to show to community members.
    Click here to open settings and give it one!": "你的社群没有一个很长的描述,一个HTML页面可以展示给社群成员。
    点击这里即可打开设置添加详细介绍!", "Failed to load %(groupId)s": "%(groupId)s 加载失败", - "This room is not public. You will not be able to rejoin without an invite.": "此聊天室不是公开聊天室。如果没有成员邀请,您将无法重新加入。", + "This room is not public. You will not be able to rejoin without an invite.": "此聊天室不是公开聊天室。如果没有成员邀请,你将无法重新加入。", "Can't leave Server Notices room": "无法退出服务器公告聊天室", - "This room is used for important messages from the Homeserver, so you cannot leave it.": "此聊天室是用于发布来自主服务器的重要讯息的,所以您不能退出它。", + "This room is used for important messages from the Homeserver, so you cannot leave it.": "此聊天室是用于发布来自主服务器的重要讯息的,所以你不能退出它。", "Terms and Conditions": "条款与要求", - "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "若要继续使用主服务器 %(homeserverDomain)s,您必须浏览并同意我们的条款与要求。", + "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "若要继续使用主服务器 %(homeserverDomain)s,你必须浏览并同意我们的条款与要求。", "Review terms and conditions": "浏览条款与要求", - "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "若要设置社区过滤器,请将社区头像拖到屏幕最左侧的社区过滤器面板上。单击社区过滤器面板中的社区头像即可过滤出与该社区相关联的房间和人员。", - "You can't send any messages until you review and agree to our terms and conditions.": "在您查看并同意 我们的条款与要求 之前,您不能发送任何消息。", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "若要设置社群过滤器,请将社群头像拖到屏幕最左侧的社群过滤器面板上。单击社群过滤器面板中的社群头像即可过滤出与此社群相关联的房间和人员。", + "You can't send any messages until you review and agree to our terms and conditions.": "在你查看并同意 我们的条款与要求 之前,你不能发送任何消息。", "Clear filter": "清除过滤器", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "尝试加载此聊天室时间轴上的某处,但您没有查看相关消息的权限。", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "尝试加载此聊天室时间轴上的某处,但你没有查看相关消息的权限。", "No Audio Outputs detected": "未检测到可用的音频输出方式", "Audio Output": "音频输出", "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "已向 %(emailAddress)s 发送了一封电子邮件。点开邮件中的链接后,请点击下面。", "Forces the current outbound group session in an encrypted room to be discarded": "强制丢弃加密聊天室中的当前出站群组会话", "Unable to connect to Homeserver. Retrying...": "无法连接至主服务器。正在重试…", - "Sorry, your homeserver is too old to participate in this room.": "抱歉,因您的主服务器的程序版本过旧,无法加入此聊天室。", + "Sorry, your homeserver is too old to participate in this room.": "抱歉,因你的主服务器的程序版本过旧,无法加入此聊天室。", "Mirror local video feed": "镜像翻转本地视频源", "This room has been replaced and is no longer active.": "此聊天室已被取代,且不再活跃。", "The conversation continues here.": "对话在这里继续。", @@ -854,14 +854,14 @@ "Legal": "法律信息", "This homeserver has hit its Monthly Active User limit.": "此主服务器已达到其每月活跃用户限制。", "This homeserver has exceeded one of its resource limits.": "本服务器已达到其使用量限制之一。", - "Please contact your service administrator to continue using this service.": "请 联系您的服务管理员 以继续使用本服务。", - "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "您的消息未被发送,因为本主服务器已达到其使用量限制之一。请 联系您的服务管理员 以继续使用本服务。", - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "您的消息未被发送,因为本主服务器已达到其每月活跃用户限制。请 联系您的服务管理员 以继续使用本服务。", - "Please contact your service administrator to continue using the service.": "请 联系您的服务管理员 以继续使用本服务。", - "Please contact your homeserver administrator.": "请 联系您的主服务器管理员。", - "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s 将此聊天室的主地址设为了 %(address)s。", - "%(senderName)s removed the main address for this room.": "%(senderName)s 移除了此聊天室的主地址。", - "Unable to load! Check your network connectivity and try again.": "无法加载!请检查您的网络连接并重试。", + "Please contact your service administrator to continue using this service.": "请 联系你的服务管理员 以继续使用本服务。", + "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "你的消息未被发送,因为本主服务器已达到其使用量限制之一。请 联系你的服务管理员 以继续使用本服务。", + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "你的消息未被发送,因为本主服务器已达到其每月活跃用户限制。请 联系你的服务管理员 以继续使用本服务。", + "Please contact your service administrator to continue using the service.": "请 联系你的服务管理员 以继续使用本服务。", + "Please contact your homeserver administrator.": "请 联系你的主服务器管理员。", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s 将此聊天室的主要地址设为了 %(address)s。", + "%(senderName)s removed the main address for this room.": "%(senderName)s 移除了此聊天室的主要地址。", + "Unable to load! Check your network connectivity and try again.": "无法加载!请检查你的网络连接并重试。", "User %(user_id)s does not exist": "用户 %(user_id)s 不存在", "There was an error joining the room": "加入聊天室时发生错误", "Custom user status messages": "自定义用户状态信息", @@ -887,7 +887,7 @@ "Download": "下载", "Retry": "重试", "Go to Settings": "打开设置", - "You do not have permission to invite people to this room.": "您没有权限将其他用户邀请至本聊天室。", + "You do not have permission to invite people to this room.": "你没有权限将其他用户邀请至本聊天室。", "Unknown server error": "未知服务器错误", "Failed to invite users to the room:": "邀请失败:", "No need for symbols, digits, or uppercase letters": "不一定要有符号、数字或大写字母", @@ -895,12 +895,12 @@ "Avoid repeated words and characters": "避免重复词语与字符", "Avoid sequences": "避免递增或递减的序列", "Avoid recent years": "避免年份", - "Avoid years that are associated with you": "避免与您相关联的年份", - "Avoid dates and years that are associated with you": "避免与您相关联的日期与年份", + "Avoid years that are associated with you": "避免与你相关联的年份", + "Avoid dates and years that are associated with you": "避免与你相关联的日期与年份", "Capitalization doesn't help very much": "大写字母并没有很大的作用", "All-uppercase is almost as easy to guess as all-lowercase": "全大写的密码通常比全小写的更容易猜测", "Reversed words aren't much harder to guess": "把单词倒过来不会比原来的难猜很多", - "Whether or not you're logged in (we don't record your username)": "您是否已经登入(我们不会记录您的用户名)", + "Whether or not you're logged in (we don't record your username)": "你是否已经登入(我们不会记录你的用户名)", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "文件 %(fileName)s 超过主服务器的文件大小限制", "Upgrades a room to a new version": "将聊天室升级到新版本", "Gets or sets the room topic": "获取或设置聊天室话题", @@ -952,7 +952,7 @@ "Show avatars in user and room mentions": "在用户和聊天室提及中显示头像", "Enable big emoji in chat": "在聊天中启用大型表情符号", "Send typing notifications": "发送正在输入通知", - "Enable Community Filter Panel": "启用社区筛选器面板", + "Enable Community Filter Panel": "启用社群筛选器面板", "Allow Peer-to-Peer for 1:1 calls": "允许一对一通话使用 P2P", "Prompt before sending invites to potentially invalid matrix IDs": "在发送邀请之前提示可能无效的 Matrix ID", "Messages containing my username": "包含我的用户名的消息", @@ -960,7 +960,7 @@ "Encrypted messages in group chats": "群聊中的加密消息", "The other party cancelled the verification.": "另一方取消了验证。", "Verified!": "已验证!", - "You've successfully verified this user.": "您已成功验证此用户。", + "You've successfully verified this user.": "你已成功验证此用户。", "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "此用户的安全消息是端到端加密的,不能被第三方读取。", "Got It": "收到", "Verify this user by confirming the following emoji appear on their screen.": "通过在其屏幕上显示以下表情符号来验证此用户。", @@ -1031,10 +1031,10 @@ "Pin": "别针", "Yes": "是", "No": "拒绝", - "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "我们已向您发送了一封电子邮件,以验证您的地址。 请按照里面的说明操作,然后单击下面的按钮。", + "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "我们已向你发送了一封电子邮件,以验证你的地址。 请按照里面的说明操作,然后单击下面的按钮。", "Email Address": "电子邮箱地址", - "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "您确定吗?如果密钥没有正确地备份您将失去您的加密消息。", - "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "加密消息已使用端对端加密保护。只有您和拥有密钥的收件人可以阅读这些消息。", + "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "你确定吗?如果密钥没有正确地备份你将失去你的加密消息。", + "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "加密消息已使用端对端加密保护。只有你和拥有密钥的收件人可以阅读这些消息。", "Unable to load key backup status": "无法载入密钥备份状态", "Restore from Backup": "从备份恢复", "Back up your keys before signing out to avoid losing them.": "在登出账号之前请备份密钥以免丢失。", @@ -1053,7 +1053,7 @@ "Language and region": "语言与区域", "Theme": "主题", "Account management": "账号管理", - "Deactivating your account is a permanent action - be careful!": "停用您的账号是一项永久性操作 - 请小心!", + "Deactivating your account is a permanent action - be careful!": "停用你的账号是一项永久性操作 - 请小心!", "General": "通用", "Credits": "感谢", "For help with using %(brand)s, click here.": "对使用 %(brand)s 的说明,请点击 这里。", @@ -1082,7 +1082,7 @@ "Developer options": "开发者选项", "Room Addresses": "聊天室地址", "Roles & Permissions": "角色与权限", - "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "历史记录阅读权限的变更只会应用到此聊天室中将来的消息。既有历史记录的可见性将不会变更。", + "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "历史记录阅读权限的更改只会应用到此聊天室中将来的消息。既有历史记录的可见性将不会更改。", "Encryption": "加密", "Once enabled, encryption cannot be disabled.": "加密一经启用,便无法禁用。", "Encrypted": "已加密", @@ -1092,32 +1092,32 @@ "Not now": "现在不要", "Don't ask me again": "不再询问", "Add some now": "立即添加", - "Error updating main address": "更新主地址时发生错误", - "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "更新聊天室的主地址时发生错误。可能是该服务器不允许,也可能是出现了一个临时错误。", - "Main address": "主地址", + "Error updating main address": "更新主要地址时发生错误", + "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "更新聊天室的主要地址时发生错误。可能是此服务器不允许,也可能是出现了一个临时错误。", + "Main address": "主要地址", "Error updating flair": "更新个性徽章时发生错误", - "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "更新此聊天室的个性徽章时发生错误。可能时该服务器不允许,也可能是发生了一个临时错误。", + "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "更新此聊天室的个性徽章时发生错误。可能时此服务器不允许,也可能是发生了一个临时错误。", "Room avatar": "聊天室头像", "Room Name": "聊天室名称", "Room Topic": "聊天室话题", "Join": "加入", "That doesn't look like a valid email address": "这看起来不像是有效的电子邮箱地址", "The following users may not exist": "以下用户可能不存在", - "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 的用户资料,你还是要邀请吗?", "Invite anyway and never warn me again": "还是邀请,不用再提醒我", "Invite anyway": "还是邀请", - "Before submitting logs, you must create a GitHub issue to describe your problem.": "在提交日志之前,您必须创建一个GitHub issue 来描述您的问题。", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "在提交日志之前,你必须创建一个GitHub issue 来描述你的问题。", "Unable to load commit detail: %(msg)s": "无法加载提交详情:%(msg)s", - "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "为避免丢失聊天记录,您必须在登出前导出房间密钥。 您需要回到较新版本的 %(brand)s 才能执行此操作", - "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "验证此用户并将其标记为已信任。在收发端到端加密消息时,信任用户可让您更加放心。", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "为避免丢失聊天记录,你必须在登出前导出房间密钥。 你需要回到较新版本的 %(brand)s 才能执行此操作", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "验证此用户并将其标记为已信任。在收发端到端加密消息时,信任用户可让你更加放心。", "Waiting for partner to confirm...": "等待对方确认中...", "Incoming Verification Request": "收到验证请求", - "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "您之前在 %(host)s 上开启了 %(brand)s 的成员列表延迟加载设置。目前版本中延迟加载功能已被停用。因为本地缓存在这两个设置项上不相容,%(brand)s 需要重新同步您的账号。", + "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "你之前在 %(host)s 上开启了 %(brand)s 的成员列表延迟加载设置。目前版本中延迟加载功能已被停用。因为本地缓存在这两个设置项上不相容,%(brand)s 需要重新同步你的账号。", "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "通过仅在需要时加载其他用户的信息,%(brand)s 现在使用的内存减少到了原来的三分之一至五分之一。 请等待与服务器重新同步!", "I don't want my encrypted messages": "我不想要我的加密消息", "Manually export keys": "手动导出密钥", - "You'll lose access to your encrypted messages": "您将失去您的加密消息的访问权", - "Are you sure you want to sign out?": "您确定要登出账号吗?", + "You'll lose access to your encrypted messages": "你将失去你的加密消息的访问权", + "Are you sure you want to sign out?": "你确定要登出账号吗?", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "如果您碰到任何错误或者希望分享您的反馈,请在 Github 上联系我们。", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "要帮助避免提交重复的 issue,请您先 查阅是否为已存在的 issue (如有则添加一个 +1 ) ,或者如果找不到则 新建一个 issue 。", "Report bugs & give feedback": "上报错误及提供反馈", @@ -1125,7 +1125,7 @@ "Room Settings - %(roomName)s": "聊天室设置 - %(roomName)s", "A username can only contain lower case letters, numbers and '=_-./'": "用户名只能包含小写字母、数字和 '=_-./'", "Failed to decrypt %(failedCount)s sessions!": "%(failedCount)s 个会话解密失败!", - "Warning: you should only set up key backup from a trusted computer.": "警告:您应该只在受信任的电脑上设置密钥备份。", + "Warning: you should only set up key backup from a trusted computer.": "警告:你应此只在受信任的电脑上设置密钥备份。", "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "通过输入恢复密码来访问您的安全消息历史记录和设置安全通信。", "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "如果忘记了恢复密码,您可以 使用恢复密钥 或者 设置新的恢复选项", "This looks like a valid recovery key!": "看起来是有效的恢复密钥!", @@ -1137,8 +1137,8 @@ "Set status": "设置状态", "Set a new status...": "设置新状态...", "Hide": "隐藏", - "This homeserver would like to make sure you are not a robot.": "此主服务器想要确认您不是机器人。", - "Please review and accept all of the homeserver's policies": "请阅读并接受该主服务器的所有政策", + "This homeserver would like to make sure you are not a robot.": "此主服务器想要确认你不是机器人。", + "Please review and accept all of the homeserver's policies": "请阅读并接受此主服务器的所有政策", "Please review and accept the policies of this homeserver:": "请阅读并接受此主服务器的政策:", "Your Modular server": "您的模组服务器", "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "输入您的模组主服务器的位置。它可能使用的是您自己的域名或者 modular.im 的子域名。", @@ -1162,14 +1162,14 @@ "Other": "其他", "Find other public servers or use a custom server": "寻找其他公共服务器或使用自定义服务器", "Couldn't load page": "无法加载页面", - "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "您是此社区的管理员。 没有其他管理员的邀请,您将无法重新加入。", - "This homeserver does not support communities": "此主服务器不支持社区功能", + "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "你是此社群的管理员。 没有其他管理员的邀请,你将无法重新加入。", + "This homeserver does not support communities": "此主服务器不支持社群功能", "Guest": "游客", "Could not load user profile": "无法加载用户资料", "Your Matrix account on %(serverName)s": "您在 %(serverName)s 上的 Matrix 账号", - "A verification email will be sent to your inbox to confirm setting your new password.": "一封验证电子邮件将发送到您的邮箱以确认您设置了新密码。", + "A verification email will be sent to your inbox to confirm setting your new password.": "一封验证电子邮件将发送到你的邮箱以确认你设置了新密码。", "Sign in instead": "登入", - "Your password has been reset.": "您的密码已重置。", + "Your password has been reset.": "你的密码已重置。", "Set a new password": "设置新密码", "Invalid homeserver discovery response": "无效的主服务器搜索响应", "Invalid identity server discovery response": "无效的身份服务器搜索响应", @@ -1182,14 +1182,14 @@ "Unable to query for supported registration methods.": "无法查询支持的注册方法。", "Create your account": "创建您的账号", "Keep going...": "请继续...", - "For maximum security, this should be different from your account password.": "为确保最大的安全性,它应该与您的账号密码不同。", + "For maximum security, this should be different from your account password.": "为确保最大的安全性,它应该与你的账号密码不同。", "That matches!": "匹配成功!", "That doesn't match.": "不匹配。", "Go back to set it again.": "返回重新设置。", "Print it and store it somewhere safe": "打印 并存放在安全的地方", "Save it on a USB key or backup drive": "保存 在 U 盘或备份磁盘中", - "Copy it to your personal cloud storage": "复制 到您的个人云端存储", - "Your keys are being backed up (the first backup could take a few minutes).": "正在备份您的密钥(第一次备份可能会花费几分钟时间)。", + "Copy it to your personal cloud storage": "复制 到你的个人云端存储", + "Your keys are being backed up (the first backup could take a few minutes).": "正在备份你的密钥(第一次备份可能会花费几分钟时间)。", "Set up Secure Message Recovery": "设置安全消息恢复", "Starting backup...": "开始备份...", "Success!": "成功!", @@ -1198,18 +1198,18 @@ "If you don't want to set this up now, you can later in Settings.": "如果您现在不想设置,您可以稍后在设置中操作。", "New Recovery Method": "新恢复方式", "A new recovery passphrase and key for Secure Messages have been detected.": "检测到安全消息的一个新恢复密码和密钥。", - "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.": "如果您没有设置新恢复方式,可能有攻击者正试图侵入您的账号。请立即更改您的账号密码并在设置中设定一个新恢复方式。", + "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.": "如果你没有设置新恢复方式,可能有攻击者正试图侵入你的账号。请立即更改你的账号密码并在设置中设定一个新恢复方式。", "Set up Secure Messages": "设置安全消息", "Recovery Method Removed": "恢复方式已移除", - "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "如果您没有移除该恢复方式,可能有攻击者正试图侵入您的账号。请立即更改您的账号密码并在设置中设定一个新的恢复方式。", + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "如果你没有移除此恢复方式,可能有攻击者正试图侵入你的账号。请立即更改你的账号密码并在设置中设定一个新的恢复方式。", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "在纯文本消息开头添加 ¯\\_(ツ)_/¯", "User %(userId)s is already in the room": "用户 %(userId)s 已在聊天室中", "The user must be unbanned before they can be invited.": "用户必须先解封才能被邀请。", - "Upgrade to your own domain": "升级 到您自己的域名", + "Upgrade to your own domain": "升级 到你自己的域名", "Accept all %(invitedRooms)s invites": "接受所有 %(invitedRooms)s 邀请", "Change room avatar": "更改聊天室头像", "Change room name": "更改聊天室名称", - "Change main address for the room": "更改聊天室主地址", + "Change main address for the room": "更改聊天室主要地址", "Change history visibility": "更改历史记录可见性", "Change permissions": "更改权限", "Change topic": "更改话题", @@ -1227,18 +1227,18 @@ "Enable encryption?": "启用加密?", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "聊天室加密一经启用,便无法禁用。在加密聊天室中,发送的消息无法被服务器看到,只能被聊天室的参与者看到。启用加密可能会使许多机器人和桥接无法正常运作。 详细了解加密。", "Power level": "权限级别", - "Want more than a community? Get your own server": "想要的不只是社区? 架设您自己的服务器", + "Want more than a community? Get your own server": "想要的不只是社群? 架设你自己的服务器", "Please install Chrome, Firefox, or Safari for the best experience.": "请安装 ChromeFirefox,或 Safari 以获得最佳体验。", - "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.": "警告:升级聊天室 不会自动将聊天室成员转移到新版聊天室中。 我们将会在旧版聊天室中发布一个新版聊天室的链接 - 聊天室成员必须点击该链接以加入新聊天室。", + "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.": "警告:升级聊天室 不会自动将聊天室成员转移到新版聊天室中。 我们将会在旧版聊天室中发布一个新版聊天室的链接 - 聊天室成员必须点击此链接以加入新聊天室。", "Adds a custom widget by URL to the room": "通过链接为聊天室添加自定义挂件", "Please supply a https:// or http:// widget URL": "请提供一个 https:// 或 http:// 形式的插件", - "You cannot modify widgets in this room.": "您无法修改此聊天室的插件。", + "You cannot modify widgets in this room.": "你无法修改此聊天室的插件。", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s 撤销了对 %(targetDisplayName)s 加入聊天室的邀请。", "Upgrade this room to the recommended room version": "升级此聊天室至推荐版本", - "This room is running room version , which this homeserver has marked as unstable.": "此聊天室运行的聊天室版本是 ,该版本已被主服务器标记为 不稳定 。", + "This room is running room version , which this homeserver has marked as unstable.": "此聊天室运行的聊天室版本是 ,此版本已被主服务器标记为 不稳定 。", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "升级此聊天室将会关闭聊天室的当前实例并创建一个具有相同名称的升级版聊天室。", "Failed to revoke invite": "撤销邀请失败", - "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "无法撤销邀请。该服务器可能出现了临时错误,或者您没有足够的权限来撤销邀请。", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "无法撤销邀请。此服务器可能出现了临时错误,或者你没有足够的权限来撤销邀请。", "Revoke invite": "撤销邀请", "Invited by %(sender)s": "被 %(sender)s 邀请", "Maximize apps": "最大化应用程序", @@ -1246,32 +1246,32 @@ "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "位于 %(widgetUrl)s 的小部件想要验证您的身份。在您允许后,小部件就可以验证您的用户 ID,但不能代您执行操作。", "Remember my selection for this widget": "记住我对此挂件的选择", "Deny": "拒绝", - "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s 无法从主服务器处获取协议列表。该主服务器上的软件可能过旧,不支持第三方网络。", + "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s 无法从主服务器处获取协议列表。此主服务器上的软件可能过旧,不支持第三方网络。", "%(brand)s failed to get the public room list.": "%(brand)s 无法获取公开聊天室列表。", "The homeserver may be unavailable or overloaded.": "主服务器似乎不可用或过载。", - "You have %(count)s unread notifications in a prior version of this room.|other": "您在此聊天室的先前版本中有 %(count)s 条未读通知。", - "You have %(count)s unread notifications in a prior version of this room.|one": "您在此聊天室的先前版本中有 %(count)s 条未读通知。", + "You have %(count)s unread notifications in a prior version of this room.|other": "你在此聊天室的先前版本中有 %(count)s 条未读通知。", + "You have %(count)s unread notifications in a prior version of this room.|one": "你在此聊天室的先前版本中有 %(count)s 条未读通知。", "Add Email Address": "添加 Email 地址", "Add Phone Number": "添加电话号码", "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "是否使用“面包屑”功能(最近访问的房间的图标在房间列表上方显示)", "Call failed due to misconfigured server": "因为服务器配置错误通话失败", - "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "请联系您主服务器(%(homeserverDomain)s)的管理员设置 TURN 服务器来确保通话运作稳定。", - "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "您也可以尝试使用turn.matrix.org公共服务器,但通话质量稍差,并且其将会得知您的 IP。您可以在设置中更改此选项。", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "请联系你主服务器(%(homeserverDomain)s)的管理员设置 TURN 服务器来确保通话运作稳定。", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "你也可以尝试使用turn.matrix.org公共服务器,但通话质量稍差,并且其将会得知你的 IP。你可以在设置中更改此选项。", "Try using turn.matrix.org": "尝试使用 turn.matrix.org", - "Your %(brand)s is misconfigured": "您的 %(brand)s 配置有错误", + "Your %(brand)s is misconfigured": "你的 %(brand)s 配置有错误", "Use Single Sign On to continue": "使用单点登录继续", - "Confirm adding this email address by using Single Sign On to prove your identity.": "通过使用单点登录来证明您的身份,并确认添加此邮件地址。", + "Confirm adding this email address by using Single Sign On to prove your identity.": "通过使用单点登录来证明你的身份,并确认添加此邮件地址。", "Single Sign On": "单点登录", "Confirm adding email": "确认使用邮件", "Click the button below to confirm adding this email address.": "点击下面的按钮以确认添加此邮箱地址。", - "Confirm adding this phone number by using Single Sign On to prove your identity.": "通过单点登录以证明您的身份,并确认添加此电话号码。", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "通过单点登录以证明你的身份,并确认添加此电话号码。", "Confirm adding phone number": "确认添加电话号码", "Click the button below to confirm adding this phone number.": "点击下面的按钮以确认添加此电话号码。", "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "是否在触屏设备上使用 %(brand)s", - "Whether you're using %(brand)s as an installed Progressive Web App": "您是否已将 %(brand)s 作为渐进式 Web 应用(PWA)安装", - "Your user agent": "您的用户代理(user agent)", + "Whether you're using %(brand)s as an installed Progressive Web App": "你是否已将 %(brand)s 作为渐进式 Web 应用(PWA)安装", + "Your user agent": "你的用户代理(user agent)", "Replying With Files": "回复文件", - "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "当前无法在回复中附加文件。您想要仅上传此文件而不回复吗?", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "当前无法在回复中附加文件。你想要仅上传此文件而不回复吗?", "The file '%(fileName)s' failed to upload.": "上传文件 ‘%(fileName)s’ 失败。", "The server does not support the room version specified.": "服务器不支持指定的聊天室版本。", "If you cancel now, you won't complete verifying the other user.": "如果现在取消,您将无法完成验证其他用户。", @@ -1283,11 +1283,11 @@ "Encryption upgrade available": "提供加密升级", "Set up encryption": "设置加密", "Review where you’re logged in": "查看您的登录位置", - "New login. Was this you?": "现在登录。请问是您本人吗?", + "New login. Was this you?": "现在登录。请问是你本人吗?", "Name or Matrix ID": "姓名或 Matrix ID", "Identity server has no terms of service": "身份服务器无服务条款", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "此操作需要访问默认的身份服务器 以验证邮箱地址或电话号码,但是此服务器无任何服务条款。", - "Only continue if you trust the owner of the server.": "只有您信任服务器所有者才能继续。", + "Only continue if you trust the owner of the server.": "只有你信任服务器所有者才能继续。", "Trust": "信任", "%(name)s is requesting verification": "%(name)s 正在请求验证", "Sign In or Create Account": "登录或创建账号", @@ -1299,12 +1299,12 @@ "Actions": "动作", "Sends a message as plain text, without interpreting it as markdown": "以纯文本形式发送消息,不将其作为 markdown 处理", "Sends a message as html, without interpreting it as markdown": "以 html 格式发送消息,不将其作为 markdown 处理", - "You do not have the required permissions to use this command.": "您没有权限使用此命令。", + "You do not have the required permissions to use this command.": "你没有权限使用此命令。", "Error upgrading room": "升级聊天室时发生错误", - "Double check that your server supports the room version chosen and try again.": "请再次检查您的服务器是否支持所选聊天室版本,然后再试一次。", + "Double check that your server supports the room version chosen and try again.": "请再次检查你的服务器是否支持所选聊天室版本,然后再试一次。", "Changes the avatar of the current room": "更改当前聊天室头像", - "Changes your avatar in this current room only": "仅改变您在当前聊天室的头像", - "Changes your avatar in all rooms": "改变您在所有聊天室的头像", + "Changes your avatar in this current room only": "仅改变你在当前聊天室的头像", + "Changes your avatar in all rooms": "改变你在所有聊天室的头像", "Failed to set topic": "话题设置失败", "Use an identity server": "使用身份服务器", "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "使用身份服务器以通过电子邮件邀请其他用户。单击继续以使用默认身份服务器(%(defaultIdentityServerName)s),或在设置中进行管理。", @@ -1317,23 +1317,23 @@ "Unknown (user, session) pair:": "未知(用户、会话)对:", "Session already verified!": "会话已验证!", "WARNING: Session already verified, but keys do NOT MATCH!": "警告:会话已验证,但密钥不匹配!", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "警告:密钥验证失败!%(userId)s 的会话 %(deviceId)s 的签名密钥为 %(fprint)s,与提供的密钥 %(fingerprint)s 不符。这可能表示您的通讯已被截获!", - "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "您提供的签名密钥与您从 %(userId)s 的会话 %(deviceId)s 获取的一致。该会话被标为已验证。", - "Sends the given message coloured as a rainbow": "以彩虹色发送给定消息", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "警告:密钥验证失败!%(userId)s 的会话 %(deviceId)s 的签名密钥为 %(fprint)s,与提供的密钥 %(fingerprint)s 不符。这可能表示你的通讯已被截获!", + "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "你提供的签名密钥与你从 %(userId)s 的会话 %(deviceId)s 获取的一致。此会话被标为已验证。", + "Sends the given message coloured as a rainbow": "此消息以彩虹色进行渲染", "Sends the given emote coloured as a rainbow": "以彩虹色发送给定表情符号", "Displays list of commands with usages and descriptions": "显示指令清单与其描述和用法", "Displays information about a user": "显示关于用户的信息", "Send a bug report with logs": "发送带日志的错误报告", "Opens chat with the given user": "与指定用户发起聊天", "Sends a message to the given user": "向指定用户发消息", - "%(senderName)s made no change.": "%(senderName)s 未做出变更。", + "%(senderName)s made no change.": "%(senderName)s 未做出更改。", "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s 将聊天室名称从 %(oldRoomName)s 改为 %(newRoomName)s。", "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s 为此聊天室添加备用地址 %(addresses)s。", "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s 为此聊天室添加了备用地址 %(addresses)s。", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s 为此聊天室移除了备用地址 %(addresses)s。", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s 为此聊天室移除了备用地址 %(addresses)s。", "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s 更改了此聊天室的备用地址。", - "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s 更改了此聊天室的主地址与备用地址。", + "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s 更改了此聊天室的主要地址与备用地址。", "%(senderName)s changed the addresses for this room.": "%(senderName)s 更改了此聊天室的地址。", "%(senderName)s placed a voice call.": "%(senderName)s 发起了语音通话。", "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s 发起了语音通话。(此浏览器不支持)", @@ -1356,25 +1356,25 @@ "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s更改了一个由于%(reason)s而禁止聊天室%(oldGlob)s跟%(newGlob)s匹配的规则", "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s 更新了一个由于%(reason)s而禁止服务器%(oldGlob)s跟%(newGlob)s匹配的规则", "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s 更新了一个由于%(reason)s而禁止%(oldGlob)s跟%(newGlob)s匹配的规则", - "You signed in to a new session without verifying it:": "您登录了未经过验证的新会话:", - "Verify your other session using one of the options below.": "使用以下选项之一验证您的其他会话。", + "You signed in to a new session without verifying it:": "你登录了未经过验证的新会话:", + "Verify your other session using one of the options below.": "使用以下选项之一验证你的其他会话。", "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s(%(userId)s)登录到未验证的新会话:", - "Ask this user to verify their session, or manually verify it below.": "要求该用户验证其会话,或在下面手动进行验证。", + "Ask this user to verify their session, or manually verify it below.": "要求此用户验证其会话,或在下面手动进行验证。", "Not Trusted": "不可信任", "Manually Verify by Text": "手动验证文字", "Interactively verify by Emoji": "通过表情符号进行交互式验证", "Done": "完成", "Cannot reach homeserver": "不可连接到主服务器", - "Ensure you have a stable internet connection, or get in touch with the server admin": "确保您的网络连接稳定,或与服务器管理员联系", - "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.": "跟您的%(brand)s管理员确认您的配置不正确或重复的条目。", + "Ensure you have a stable internet connection, or get in touch with the server admin": "确保你的网络连接稳定,或与服务器管理员联系", + "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.": "跟你的%(brand)s管理员确认你的配置不正确或重复的条目。", "Cannot reach identity server": "不可连接到身份服务器", "Room name or address": "房间名称或地址", "Joins room with given address": "使用给定地址加入房间", "Verify this login": "验证此登录名", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "通过从其他会话之一验证此登录名并授予其访问加密信息的权限来确认您的身份。", - "Which officially provided instance you are using, if any": "如果您在使用官方实例,是哪一个", - "Every page you use in the app": "您在应用中使用的每个页面", - "Are you sure you want to cancel entering passphrase?": "您确定要取消输入密语吗?", + "Which officially provided instance you are using, if any": "如果有的话,你正在使用官方所提供的哪个实例", + "Every page you use in the app": "你在应用中使用的每个页面", + "Are you sure you want to cancel entering passphrase?": "你确定要取消输入密语吗?", "Go Back": "后退", "Use your account to sign in to the latest version": "使用您的帐户登录到最新版本", "We’re excited to announce Riot is now Element": "我们很高兴地宣布Riot现在更名为Element", @@ -1382,9 +1382,9 @@ "Unrecognised room address:": "无法识别的聊天室地址:", "Light": "浅色", "Dark": "深色", - "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "您可以注册,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", - "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "您可以重置密码,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", - "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "您可以登录,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "你可以注册,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "你可以重置密码,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "你可以登录,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", "No homeserver URL provided": "未输入主服务器链接", "Unexpected error resolving homeserver configuration": "解析主服务器配置时发生未知错误", "Unexpected error resolving identity server configuration": "解析身份服务器配置时发生未知错误", @@ -1404,17 +1404,17 @@ "about a day from now": "从现在开始约一天", "%(num)s days from now": "从现在开始%(num)s天", "%(name)s (%(userId)s)": "%(name)s%(userId)s", - "Your browser does not support the required cryptography extensions": "您的浏览器不支持所需的密码学扩展", - "The user's homeserver does not support the version of the room.": "用户的主服务器不支持该聊天室版本。", + "Your browser does not support the required cryptography extensions": "你的浏览器不支持所需的密码学扩展", + "The user's homeserver does not support the version of the room.": "用户的主服务器不支持此聊天室版本。", "Help us improve %(brand)s": "请协助我们改进%(brand)s", "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "发送匿名使用情况数据,以协助我们改进%(brand)s。这将使用cookie。", "I want to help": "我乐意协助", "Verify all your sessions to ensure your account & messages are safe": "验证您的所有会话,以确保账号和消息安全", "Review": "开始验证", "Later": "稍后再说", - "Your homeserver has exceeded its user limit.": "您的主服务器已超过用户限制。", - "Your homeserver has exceeded one of its resource limits.": "您的主服务器已超过某项资源限制。", - "Contact your server admin.": "请联系您的服务器管理员。", + "Your homeserver has exceeded its user limit.": "你的主服务器已超过用户限制。", + "Your homeserver has exceeded one of its resource limits.": "你的主服务器已超过某项资源限制。", + "Contact your server admin.": "请联系你的服务器管理员。", "Ok": "确定", "Set password": "设置密码", "To return to your account in future you need to set a password": "要在之后取回您的帐户,您需要设置密码", @@ -1426,13 +1426,13 @@ "Restart": "重启应用", "Upgrade your %(brand)s": "升级您的%(brand)s", "A new version of %(brand)s is available!": "发现%(brand)s的新版本!", - "You joined the call": "您加入通话", + "You joined the call": "你加入通话", "%(senderName)s joined the call": "%(senderName)s加入通话", "Call in progress": "通话中", "You left the call": "您离开了通话", "%(senderName)s left the call": "%(senderName)s离开了通话", "Call ended": "通话结束", - "You started a call": "您开始了通话", + "You started a call": "你开始了通话", "%(senderName)s started a call": "%(senderName)s开始了通话", "Waiting for answer": "等待接听", "%(senderName)s is calling": "%(senderName)s正在通话", @@ -1461,35 +1461,35 @@ "or": "或者", "Start": "开始", "Confirm the emoji below are displayed on both sessions, in the same order:": "确认两个会话上都以同样顺序显示了下面的emoji:", - "The person who invited you already left the room.": "邀请您的人已经离开了聊天室。", - "The person who invited you already left the room, or their server is offline.": "邀请您的人已经离开了聊天室,或者其服务器为离线状态。", + "The person who invited you already left the room.": "邀请你的人已经离开了聊天室。", + "The person who invited you already left the room, or their server is offline.": "邀请你的人已经离开了聊天室,或者其服务器为离线状态。", "Change notification settings": "修改通知设置", "Manually verify all remote sessions": "手动验证所有远程会话", "My Ban List": "我的封禁列表", - "This is your list of users/servers you have blocked - don't leave the room!": "这是您屏蔽的用户和服务器的列表——请不要离开此聊天室!", + "This is your list of users/servers you have blocked - don't leave the room!": "这是你屏蔽的用户和服务器的列表——请不要离开此聊天室!", "Unknown caller": "未知来电人", "Incoming voice call": "语音来电", "Incoming video call": "视频来电", "Incoming call": "来电", - "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "等待您的另一个会话 %(deviceName)s (%(deviceId)s) 进行验证…", - "Waiting for your other session to verify…": "等待您的另一个会话进行验证…", + "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "等待你的另一个会话 %(deviceName)s (%(deviceId)s) 进行验证…", + "Waiting for your other session to verify…": "等待你的另一个会话进行验证…", "Waiting for %(displayName)s to verify…": "等待 %(displayName)s 进行验证…", "Cancelling…": "正在取消…", "They match": "它们匹配", "They don't match": "它们不匹配", "To be secure, do this in person or use a trusted way to communicate.": "为了安全,请当面完成或使用信任的方法交流。", "Lock": "锁", - "Your server isn't responding to some requests.": "您的服务器没有响应一些请求。", + "Your server isn't responding to some requests.": "你的服务器没有响应一些请求。", "From %(deviceName)s (%(deviceId)s)": "来自 %(deviceName)s (%(deviceId)s)", "Decline (%(counter)s)": "拒绝 (%(counter)s)", "Accept to continue:": "接受 以继续:", "Upload": "上传", "Show less": "显示更少", "Show more": "显示更多", - "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "修改密码会重置所有会话上的端对端加密的密钥,使加密聊天记录不可读,除非您先导出您的聊天室密钥,之后再重新导入。在未来会有所改进。", - "Your homeserver does not support cross-signing.": "您的主服务器不支持交叉签名。", + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "修改密码会重置所有会话上的端对端加密的密钥,使加密聊天记录不可读,除非你先导出你的聊天室密钥,之后再重新导入。在未来会有所改进。", + "Your homeserver does not support cross-signing.": "你的主服务器不支持交叉签名。", "Cross-signing and secret storage are enabled.": "交叉签名和秘密存储已启用。", - "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "您的账号在秘密存储中有交叉签名身份,但并没有被此会话信任。", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "你的账号在秘密存储中有交叉签名身份,但并没有被此会话信任。", "Cross-signing and secret storage are not yet set up.": "交叉签名和秘密存储尚未设置。", "Reset cross-signing and secret storage": "重置交叉签名和秘密存储", "Bootstrap cross-signing and secret storage": "自举交叉签名和秘密存储", @@ -1505,7 +1505,7 @@ "Secret storage public key:": "秘密存储公钥:", "in account data": "在账号数据中", "exists": "存在", - "Your homeserver does not support session management.": "您的主服务器不支持会话管理。", + "Your homeserver does not support session management.": "你的主服务器不支持会话管理。", "Unable to load session list": "无法加载会话列表", "Confirm deleting these sessions": "确认删除这些会话", "Click the button below to confirm deleting these sessions.|other": "点击下方按钮以确认删除这些会话。", @@ -1518,9 +1518,9 @@ "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "逐一验证用户的每一个会话以将其标记为已信任,而不信任交叉签名的设备。", "Manage": "管理", "Enable": "启用", - "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s 缺少安全地在本地缓存加密信息所必须的部件。如果您想实验此功能,请构建一个自定义的带有搜索部件的 %(brand)s 桌面版。", + "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s 缺少安全地在本地缓存加密信息所必须的部件。如果你想实验此功能,请构建一个自定义的带有搜索部件的 %(brand)s 桌面版。", "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s 在浏览器中运行时不能安全地在本地缓存加密信息。请使用%(brand)s 桌面版以使加密信息出现在搜索结果中。", - "This session is backing up your keys. ": "此会话正在备份您的密钥。 ", + "This session is backing up your keys. ": "此会话正在备份你的密钥。 ", "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "在登出前连接此会话到密钥备份以避免丢失可能仅在此会话上的密钥。", "Connect this session to Key Backup": "将此会话连接到密钥备份", "Backup has a valid signature from this user": "备份有来自此用户的有效签名", @@ -1533,13 +1533,13 @@ "Backup has a valid signature from unverified session ": "备份有一个有效的签名,它来自未验证的会话", "Backup has an invalid signature from verified session ": "备份有一个无效的签名,它来自已验证的会话", "Backup has an invalid signature from unverified session ": "备份有一个无效的签名,它来自未验证的会话", - "Backup is not signed by any of your sessions": "备份没有被您的任何一个会话签名", + "Backup is not signed by any of your sessions": "备份没有被你的任何一个会话签名", "This backup is trusted because it has been restored on this session": "此备份是受信任的因为它被恢复到了此会话上", "Backup key stored: ": "存储的备份密钥: ", - "Your keys are not being backed up from this session.": "您的密钥没有被此会话备份。", + "Your keys are not being backed up from this session.": "你的密钥没有被此会话备份。", "Clear notifications": "清除通知", "There are advanced notifications which are not shown here.": "有高级通知没有显示在此处。", - "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "您可能在非 %(brand)s 的客户端里配置了它们。您在 %(brand)s 里无法修改它们,但它们仍然适用。", + "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "你可能在非 %(brand)s 的客户端里配置了它们。你在 %(brand)s 里无法修改它们,但它们仍然适用。", "Enable desktop notifications for this session": "为此会话启用桌面通知", "Enable audible notifications for this session": "为此会话启用声音通知", "Identity Server URL must be HTTPS": "身份服务器连接必须是 HTTPS", @@ -1549,29 +1549,29 @@ "Change identity server": "更改身份服务器", "Disconnect from the identity server and connect to instead?": "从 身份服务器断开连接并连接到 吗?", "Terms of service not accepted or the identity server is invalid.": "服务协议未同意或身份服务器无效。", - "The identity server you have chosen does not have any terms of service.": "您选择的身份服务器没有服务协议。", + "The identity server you have chosen does not have any terms of service.": "你选择的身份服务器没有服务协议。", "Disconnect identity server": "断开身份服务器连接", "Disconnect from the identity server ?": "从身份服务器 断开连接吗?", "Disconnect": "断开连接", "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "断开连接前,你应当删除你的个人信息从身份服务器。不幸的是,身份服务器当前处于离线状态或无法访问。", - "You should:": "您应该:", + "You should:": "你应该:", "contact the administrators of identity server ": "联系身份服务器 的管理员", "wait and try again later": "等待并稍后重试", "Disconnect anyway": "仍然断开连接", - "You are still sharing your personal data on the identity server .": "您仍然在分享您的个人信息在身份服务器上。", - "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "我们推荐您在断开连接前从身份服务器上删除您的邮箱地址和电话号码。", + "You are still sharing your personal data on the identity server .": "你仍然在分享你的个人信息在身份服务器上。", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "我们推荐你在断开连接前从身份服务器上删除你的邮箱地址和电话号码。", "Identity Server (%(server)s)": "身份服务器(%(server)s)", "not stored": "未存储", - "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "您正在使用 以发现您认识的现存联系人并被其发现。您可以在下方更改您的身份服务器。", - "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "如果您不想使用 以发现您认识的现存联系人并被其发现,请在下方输入另一个身份服务器。", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "你正在使用 以发现你认识的现存联系人并被其发现。你可以在下方更改你的身份服务器。", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "如果你不想使用 以发现你认识的现存联系人并被其发现,请在下方输入另一个身份服务器。", "Identity Server": "身份服务器", - "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "您现在没有使用身份服务器。若想发现您认识的现存联系人并被其发现,请在下方添加一个身份服务器。", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "从您的身份服务器断开连接意味着您将不可被别的用户发现,同时您也将不能用邮箱或电话邀请别人。", - "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "使用身份服务器是可选的。如果您选择不使用身份服务器,您将不能被别的用户发现,也不能用邮箱或电话邀请别人。", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "你现在没有使用身份服务器。若想发现你认识的现存联系人并被其发现,请在下方添加一个身份服务器。", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "从你的身份服务器断开连接意味着你将不可被别的用户发现,同时你也将不能用邮箱或电话邀请别人。", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "使用身份服务器是可选的。如果你选择不使用身份服务器,你将不能被别的用户发现,也不能用邮箱或电话邀请别人。", "Do not use an identity server": "不使用身份服务器", "Enter a new identity server": "输入一个新的身份服务器", "New version available. Update now.": "新版本可用。现在更新。", - "Hey you. You're the best!": "嘿呀。您就是最棒的!", + "Hey you. You're the best!": "嘿呀。你就是最棒的!", "Size must be a number": "大小必须是数字", "Custom font size can only be between %(min)s pt and %(max)s pt": "自定义字体大小只能介于 %(min)s pt 和 %(max)s pt 之间", "Error downloading theme information.": "下载主题信息时发生错误。", @@ -1581,10 +1581,10 @@ "Message layout": "信息布局", "Compact": "紧凑", "Modern": "现代", - "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "设置一个安装在您的系统上的字体名称,%(brand)s 会尝试使用它。", - "Customise your appearance": "自定义您的外观", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "设置一个安装在你的系统上的字体名称,%(brand)s 会尝试使用它。", + "Customise your appearance": "自定义你的外观", "Appearance Settings only affect this %(brand)s session.": "外观设置仅会影响此 %(brand)s 会话。", - "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "您的密码已成功更改。在您重新登录别的会话之前,您将不会在那里收到推送通知", + "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "你的密码已成功更改。在你重新登录别的会话之前,你将不会在那里收到推送通知", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "同意身份服务器(%(serverName)s)的服务协议以允许自己被通过邮件地址或电话号码发现。", "Discovery": "发现", "Clear cache and reload": "清除缓存重新加载", @@ -1600,22 +1600,22 @@ "Ban list rules - %(roomName)s": "封禁列表规则 - %(roomName)s", "Server rules": "服务器规则", "User rules": "用户规则", - "You have not ignored anyone.": "您没有忽略任何人。", - "You are currently ignoring:": "您正在忽略:", - "You are not subscribed to any lists": "您没有订阅任何列表", + "You have not ignored anyone.": "你没有忽略任何人。", + "You are currently ignoring:": "你正在忽略:", + "You are not subscribed to any lists": "你没有订阅任何列表", "Unsubscribe": "取消订阅", "View rules": "查看规则", - "You are currently subscribed to:": "您正在订阅:", + "You are currently subscribed to:": "你正在订阅:", "⚠ These settings are meant for advanced users.": "⚠ 这些设置是为高级用户准备的。", - "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.": "在此处添加您想忽略的用户和服务器。使用星号以使 %(brand)s 匹配任何字符。例如,@bot:* 会忽略在任何服务器上以「bot」为名的用户。", - "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.": "忽略人是通过含有封禁规则的封禁列表来完成的。订阅一个封禁列表意味着被该列表阻止的用户/服务器将会对您隐藏。", + "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.": "在此处添加你想忽略的用户和服务器。使用星号以使 %(brand)s 匹配任何字符。例如,@bot:* 会忽略在任何服务器上以「bot」为名的用户。", + "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.": "忽略人是通过含有封禁规则的封禁列表来完成的。订阅一个封禁列表意味着被此列表阻止的用户/服务器将会对你隐藏。", "Personal ban list": "个人封禁列表", - "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "您的个人封禁列表包含所有您不想看见信息的用户/服务器。您第一次忽略用户/服务器。后,一个名叫「我的封禁列表」的新聊天室将会显示在您的聊天室列表中——留在该聊天室以保持该封禁列表生效。", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "你的个人封禁列表包含所有你不想看见信息的用户/服务器。你第一次忽略用户/服务器。后,一个名叫「我的封禁列表」的新聊天室将会显示在你的聊天室列表中——留在此聊天室以保持此封禁列表生效。", "Server or user ID to ignore": "要忽略的服务器或用户 ID", "eg: @bot:* or example.org": "例如: @bot:* 或 example.org", "Subscribed lists": "订阅的列表", - "Subscribing to a ban list will cause you to join it!": "订阅一个封禁列表会使您加入它!", - "If this isn't what you want, please use a different tool to ignore users.": "如果这不是您想要的,请使用别的的工具来忽略用户。", + "Subscribing to a ban list will cause you to join it!": "订阅一个封禁列表会使你加入它!", + "If this isn't what you want, please use a different tool to ignore users.": "如果这不是你想要的,请使用别的的工具来忽略用户。", "Room ID or address of ban list": "封禁列表的聊天室 ID 或地址", "Subscribe": "订阅", "Always show the window menu bar": "总是显示窗口菜单栏", @@ -1624,8 +1624,8 @@ "Session key:": "会话密钥:", "Message search": "信息搜索", "Cross-signing": "交叉签名", - "Where you’re logged in": "您在何处登录", - "A session's public name is visible to people you communicate with": "会话的公共名称会对和您交流的人显示", + "Where you’re logged in": "你在何处登录", + "A session's public name is visible to people you communicate with": "会话的公共名称会对和你交流的人显示", "this room": "此聊天室", "View older messages in %(roomName)s.": "查看 %(roomName)s 里更旧的信息。", "Uploaded sound": "已上传的声音", @@ -1637,26 +1637,26 @@ "Upgrade the room": "更新聊天室", "Enable room encryption": "启用聊天室加密", "Error changing power level requirement": "更改权限级别需求时出错", - "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "更改此聊天室的权限级别需求时出错。请确保您有足够的权限后重试。", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "更改此聊天室的权限级别需求时出错。请确保你有足够的权限后重试。", "Error changing power level": "更改权限级别时出错", - "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "更改此用户的权限级别时出错。请确保您有足够权限后重试。", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "更改此用户的权限级别时出错。请确保你有足够权限后重试。", "To link to this room, please add an address.": "要链接至此聊天室,请添加一个地址。", "Unable to share email address": "无法共享邮件地址", - "Your email address hasn't been verified yet": "您的邮件地址尚未被验证", - "Click the link in the email you received to verify and then click continue again.": "请点击您收到的邮件中的链接后再点击继续。", - "Verify the link in your inbox": "验证您的收件箱中的链接", + "Your email address hasn't been verified yet": "你的邮件地址尚未被验证", + "Click the link in the email you received to verify and then click continue again.": "请点击你收到的邮件中的链接后再点击继续。", + "Verify the link in your inbox": "验证你的收件箱中的链接", "Complete": "完成", "Share": "共享", - "Discovery options will appear once you have added an email above.": "您在上方添加邮箱后发现选项将会出现。", + "Discovery options will appear once you have added an email above.": "你在上方添加邮箱后发现选项将会出现。", "Unable to share phone number": "无法共享电话号码", "Please enter verification code sent via text.": "请输入短信中发送的验证码。", - "Discovery options will appear once you have added a phone number above.": "您添加电话号码后发现选项将会出现。", + "Discovery options will appear once you have added a phone number above.": "你添加电话号码后发现选项将会出现。", "Remove %(email)s?": "删除 %(email)s 吗?", "Remove %(phone)s?": "删除 %(phone)s 吗?", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "一封短信已发送至 +%(msisdn)s。请输入其中包含的验证码。", "This user has not verified all of their sessions.": "此用户没有验证其全部会话。", - "You have not verified this user.": "您没有验证此用户。", - "You have verified this user. This user has verified all of their sessions.": "您验证了此用户。此用户已验证了其全部会话。", + "You have not verified this user.": "你没有验证此用户。", + "You have verified this user. This user has verified all of their sessions.": "你验证了此用户。此用户已验证了其全部会话。", "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s", "%(senderName)s: %(message)s": "%(senderName)s: %(message)s", "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", @@ -1670,33 +1670,33 @@ "Show shortcuts to recently viewed rooms above the room list": "在聊天室列表上方显示最近浏览过的聊天室的快捷方式", "Show hidden events in timeline": "显示时间线中的隐藏事件", "Low bandwidth mode": "低带宽模式", - "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "当您的主服务器没有提供通话辅助服务器时使用备用的 turn.matrix.org 服务器(您的IP地址会在通话期间被共享)", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "当你的主服务器没有提供通话辅助服务器时使用备用的 turn.matrix.org 服务器(你的IP地址会在通话期间被共享)", "Send read receipts for messages (requires compatible homeserver to disable)": "为消息发送已读回执(需要兼容的主服务器方可禁用)", "Scan this unique code": "扫描此唯一代码", "Compare unique emoji": "比较唯一表情符号", - "Compare a unique set of emoji if you don't have a camera on either device": "若您在两个设备上都没有相机,比较唯一一组表情符号", + "Compare a unique set of emoji if you don't have a camera on either device": "若你在两个设备上都没有相机,比较唯一一组表情符号", "Verify this session by confirming the following number appears on its screen.": "确认下方数字显示在屏幕上以验证此会话。", "This bridge is managed by .": "此桥接由 管理。", "Homeserver feature support:": "主服务器功能支持:", - "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "通过单点登录证明您的身份并确认删除这些会话。", - "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "通过单点登录证明您的身份并确认删除此会话。", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "通过单点登录证明你的身份并确认删除这些会话。", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "通过单点登录证明你的身份并确认删除此会话。", "Public Name": "公开名称", "Securely cache encrypted messages locally for them to appear in search results.": "在本地安全地缓存加密消息以使其出现在搜索结果中。", "Connecting to integration manager...": "正在连接至集成管理器...", "Cannot connect to integration manager": "不能连接到集成管理器", - "The integration manager is offline or it cannot reach your homeserver.": "此集成管理器为离线状态或者其不能访问您的主服务器。", - "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "检查您的浏览器是否安装有可能屏蔽身份服务器的插件(例如 Privacy Badger)", + "The integration manager is offline or it cannot reach your homeserver.": "此集成管理器为离线状态或者其不能访问你的主服务器。", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "检查你的浏览器是否安装有可能屏蔽身份服务器的插件(例如 Privacy Badger)", "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用集成管理器 (%(serverName)s) 以管理机器人、挂件和贴图集。", "Use an Integration Manager to manage bots, widgets, and sticker packs.": "使用集成管理器以管理机器人、挂件和贴图集。", "Manage integrations": "管理集成", - "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "集成管理器接收配置数据,并可以以您的名义修改挂件、发送聊天室邀请及设置权限级别。", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "集成管理器接收配置数据,并可以以你的名义修改挂件、发送聊天室邀请及设置权限级别。", "Use between %(min)s pt and %(max)s pt": "请使用介于 %(min)s pt 和 %(max)s pt 之间的大小", "Deactivate account": "停用账号", "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "要报告 Matrix 相关的安全问题,请阅读 Matrix.org 的安全公开策略。", - "Something went wrong. Please try again or view your console for hints.": "出现问题。请重试或查看您的终端以获得提示。", - "Please try again or view your console for hints.": "请重试或查看您的终端以获得提示。", - "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "您的服务器管理员未在私人聊天室和私聊中默认启用端对端加密。", - "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "在下方管理会话名称,登出您的会话或在您的用户资料中验证它们。", + "Something went wrong. Please try again or view your console for hints.": "出现问题。请重试或查看你的终端以获得提示。", + "Please try again or view your console for hints.": "请重试或查看你的终端以获得提示。", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "你的服务器管理员未在私人聊天室和私聊中默认启用端对端加密。", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "在下方管理会话名称,登出你的会话或在你的用户资料中验证它们。", "This room is bridging messages to the following platforms. Learn more.": "此聊天室正将消息桥接到以下平台。了解更多。", "This room isn’t bridging messages to any platforms. Learn more.": "此聊天室未将消息桥接到任何平台。了解更多。", "Bridges": "桥接", @@ -1704,10 +1704,10 @@ "This room is end-to-end encrypted": "此聊天室是端对端加密的", "Everyone in this room is verified": "聊天室中所有人都已被验证", "Edit message": "编辑消息", - "Your key share request has been sent - please check your other sessions for key share requests.": "您的密钥共享请求已发送——请检查您别的会话。", - "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "密钥共享请求会自动发送至您别的会话。如果您拒绝或忽略了您别的会话上的密钥共享请求,请点击此处重新为此会话请求密钥。", - "If your other sessions do not have the key for this message you will not be able to decrypt them.": "如果您别的会话没有此消息的密钥您将不能解密它们。", - "Re-request encryption keys from your other sessions.": "从您别的会话重新请求加密密钥。", + "Your key share request has been sent - please check your other sessions for key share requests.": "你的密钥共享请求已发送——请检查你别的会话。", + "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "密钥共享请求会自动发送至你别的会话。如果你拒绝或忽略了你别的会话上的密钥共享请求,请点击此处重新为此会话请求密钥。", + "If your other sessions do not have the key for this message you will not be able to decrypt them.": "如果你别的会话没有此消息的密钥你将不能解密它们。", + "Re-request encryption keys from your other sessions.": "从你别的会话重新请求加密密钥。", "This message cannot be decrypted": "此消息无法被解密", "Encrypted by an unverified session": "由未验证的会话加密", "Unencrypted": "未加密", @@ -1715,7 +1715,7 @@ "The authenticity of this encrypted message can't be guaranteed on this device.": "此加密消息的权威性不能在此设备上被保证。", "Scroll to most recent messages": "滚动到最近的消息", "Close preview": "关闭预览", - "Emoji picker": "表情符号选择器", + "Emoji picker": "Emoji 选择器", "Send a reply…": "发送回复…", "Send a message…": "发送消息…", "Bold": "粗体", @@ -1733,32 +1733,32 @@ "Join the conversation with an account": "使用一个账号加入对话", "Sign Up": "注册", "Loading room preview": "正在加载聊天室预览", - "You were kicked from %(roomName)s by %(memberName)s": "您被 %(memberName)s 踢出了 %(roomName)s", + "You were kicked from %(roomName)s by %(memberName)s": "你被 %(memberName)s 踢出了 %(roomName)s", "Reason: %(reason)s": "原因:%(reason)s", "Forget this room": "忘记此聊天室", "Re-join": "重新加入", - "You were banned from %(roomName)s by %(memberName)s": "您被 %(memberName)s 从 %(roomName)s 封禁了", - "Something went wrong with your invite to %(roomName)s": "您到 %(roomName)s 的邀请出错", - "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)。您可以将此信息转告给聊天室管理员。", + "You were banned from %(roomName)s by %(memberName)s": "你被 %(memberName)s 从 %(roomName)s 封禁了", + "Something went wrong with your invite to %(roomName)s": "你到 %(roomName)s 的邀请出错", + "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)。你可以将此信息转告给聊天室管理员。", "Try to join anyway": "仍然尝试加入", - "You can still join it because this is a public room.": "您仍然能加入,因为这是一个公共聊天室。", + "You can still join it because this is a public room.": "你仍然能加入,因为这是一个公共聊天室。", "Join the discussion": "加入讨论", - "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "这个到 %(roomName)s 的邀请是发送给 %(email)s 的,而此邮箱没有关联您的账号", - "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 的,而此邮箱没有关联你的账号", + "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": "这个到 %(roomName)s 的邀请是发送给 %(email)s 的", "Use an identity server in Settings to receive invites directly in %(brand)s.": "要直接在 %(brand)s 中接收邀请,请在设置中使用一个身份服务器。", "Share this email in Settings to receive invites directly in %(brand)s.": "要在 %(brand)s 中直接接收邀请,请在设置中共享此邮箱。", - "Do you want to chat with %(user)s?": "您想和 %(user)s 聊天吗?", + "Do you want to chat with %(user)s?": "你想和 %(user)s 聊天吗?", " wants to chat": " 想聊天", "Start chatting": "开始聊天", - "Do you want to join %(roomName)s?": "您想加入 %(roomName)s 吗?", - " invited you": " 邀请了您", + "Do you want to join %(roomName)s?": "你想加入 %(roomName)s 吗?", + " invited you": " 邀请了你", "Reject & Ignore user": "拒绝并忽略用户", - "You're previewing %(roomName)s. Want to join it?": "您正在预览 %(roomName)s。想加入吗?", - "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s 不能被预览。您想加入吗?", - "This room doesn't exist. Are you sure you're at the right place?": "此聊天室不存在。您确定您在正确的地方吗?", - "Try again later, or ask a room admin to check if you have access.": "请稍后重试,或询问聊天室管理员以检查您是否有权限。", - "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "尝试访问该房间是返回了 %(errcode)s。如果您认为您看到此消息是个错误,请提交一个错误报告。", + "You're previewing %(roomName)s. Want to join it?": "你正在预览 %(roomName)s。想加入吗?", + "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s 不能被预览。你想加入吗?", + "This room doesn't exist. Are you sure you're at the right place?": "此聊天室不存在。你确定你在正确的地方吗?", + "Try again later, or ask a room admin to check if you have access.": "请稍后重试,或询问聊天室管理员以检查你是否有权限。", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "尝试访问该房间是返回了 %(errcode)s。如果你认为你看到此消息是个错误,请提交一个错误报告。", "Appearance": "外观", "Show rooms with unread messages first": "优先显示有未读消息的聊天室", "Show previews of messages": "显示消息预览", @@ -1788,41 +1788,41 @@ "This room has already been upgraded.": "此聊天室已经被升级。", "Unknown Command": "未知命令", "Unrecognised command: %(commandText)s": "未识别的命令:%(commandText)s", - "You can use /help to list available commands. Did you mean to send this as a message?": "您可以使用 /help 列出可用命令。您是否要将其作为消息发送?", - "Hint: Begin your message with // to start it with a slash.": "提示:以 // 开始您的消息来使其以一个斜杠开始。", + "You can use /help to list available commands. Did you mean to send this as a message?": "你可以使用 /help 列出可用命令。你是否要将其作为消息发送?", + "Hint: Begin your message with // to start it with a slash.": "提示:以 // 开始你的消息来使其以一个斜杠开始。", "Send as message": "作为消息发送", "Failed to connect to integration manager": "连接至集成管理器失败", "Mark all as read": "标记所有为已读", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "更新此聊天室的备用地址时出现错误。可能是服务器不允许,也可能是出现了一个暂时的错误。", "Error creating address": "创建地址时出现错误", "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "创建地址时出现错误。可能是服务器不允许,也可能是出现了一个暂时的错误。", - "You don't have permission to delete the address.": "您没有权限删除此地址。", + "You don't have permission to delete the address.": "你没有权限删除此地址。", "There was an error removing that address. It may no longer exist or a temporary error occurred.": "删除那个地址时出现错误。可能它已不存在,也可能出现了一个暂时的错误。", "Error removing address": "删除地址时出现错误", "Local address": "本地地址", "Published Addresses": "发布的地址", - "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "发布的地址可以被任何服务器上的任何人用来加入您的聊天室。要发布一个地址,它必须先被设为一个本地地址。", + "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "发布的地址可以被任何服务器上的任何人用来加入你的聊天室。要发布一个地址,它必须先被设为一个本地地址。", "Other published addresses:": "其它发布的地址:", "No other published addresses yet, add one below": "还没有别的发布的地址,可在下方添加", "New published address (e.g. #alias:server)": "新的发布的地址(例如 #alias:server)", "Local Addresses": "本地地址", - "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "为此聊天室设置地址以便用户通过您的主服务器(%(localDomain)s)找到此聊天室", + "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "为此聊天室设置地址以便用户通过你的主服务器(%(localDomain)s)找到此聊天室", "Waiting for you to accept on your other session…": "等待您在您别的会话上接受…", "Waiting for %(displayName)s to accept…": "等待 %(displayName)s 接受…", "Accepting…": "正在接受…", "Start Verification": "开始验证", "Messages in this room are end-to-end encrypted.": "此聊天室内的消息是端对端加密的。", - "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "您的消息是安全的,只有您和收件人有解开它们的唯一密钥。", + "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "你的消息是安全的,只有你和收件人有解开它们的唯一密钥。", "Messages in this room are not end-to-end encrypted.": "此聊天室内的消息未端对端加密。", - "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "在加密聊天室中,您的消息是安全的,只有您和收件人有解开它们的唯一密钥。", + "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "在加密聊天室中,你的消息是安全的,只有你和收件人有解开它们的唯一密钥。", "Verify User": "验证用户", - "For extra security, verify this user by checking a one-time code on both of your devices.": "为了更加安全,通过在您二者的设备上检查一次性代码来验证此用户。", - "Your messages are not secure": "您的消息不安全", + "For extra security, verify this user by checking a one-time code on both of your devices.": "为了更加安全,通过在你二者的设备上检查一次性代码来验证此用户。", + "Your messages are not secure": "你的消息不安全", "One of the following may be compromised:": "以下之一可能被损害:", - "Your homeserver": "您的主服务器", - "The homeserver the user you’re verifying is connected to": "您在验证的用户连接到的主服务器", - "Yours, or the other users’ internet connection": "您的或另一位用户的网络连接", - "Yours, or the other users’ session": "您的或另一位用户的会话", + "Your homeserver": "你的主服务器", + "The homeserver the user you’re verifying is connected to": "你在验证的用户连接到的主服务器", + "Yours, or the other users’ internet connection": "你的或另一位用户的网络连接", + "Yours, or the other users’ session": "你的或另一位用户的会话", "Trusted": "受信任的", "Not trusted": "不受信任的", "%(count)s verified sessions|other": "%(count)s 个已验证的会话", @@ -1835,39 +1835,39 @@ "No recent messages by %(user)s found": "没有找到 %(user)s 最近发送的消息", "Try scrolling up in the timeline to see if there are any earlier ones.": "请尝试在时间线中向上滚动以查看是否有更早的。", "Remove recent messages by %(user)s": "删除 %(user)s 最近发送的消息", - "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "您将删除 %(user)s 发送的 %(count)s 条消息。此操作不能撤销。您想继续吗?", - "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "您将删除 %(user)s 发送的 1 条消息。此操作不能撤销。您想继续吗?", - "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "对于大量消息,可能会消耗一段时间。在此期间请不要刷新您的客户端。", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "你将删除 %(user)s 发送的 %(count)s 条消息。此操作不能撤销。你想继续吗?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "你将删除 %(user)s 发送的 1 条消息。此操作不能撤销。你想继续吗?", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "对于大量消息,可能会消耗一段时间。在此期间请不要刷新你的客户端。", "Remove %(count)s messages|other": "删除 %(count)s 条消息", "Remove %(count)s messages|one": "删除 1 条消息", "Remove recent messages": "删除最近消息", "%(role)s in %(roomName)s": "%(roomName)s 中的 %(role)s", "Deactivate user?": "停用用户吗?", - "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "停用此用户将会使其登出并阻止其再次登入。而且此用户也会离开其所在的所有聊天室。此操作不可逆。您确定要停用此用户吗?", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "停用此用户将会使其登出并阻止其再次登入。而且此用户也会离开其所在的所有聊天室。此操作不可逆。你确定要停用此用户吗?", "Deactivate user": "停用用户", "Failed to deactivate user": "停用用户失败", "This client does not support end-to-end encryption.": "此客户端不支持端对端加密。", "Security": "安全", "Verify by scanning": "扫码验证", - "Ask %(displayName)s to scan your code:": "请 %(displayName)s 扫描您的代码:", - "If you can't scan the code above, verify by comparing unique emoji.": "如果您不能扫描以上代码,请通过比较唯一的表情符号来验证。", + "Ask %(displayName)s to scan your code:": "请 %(displayName)s 扫描你的代码:", + "If you can't scan the code above, verify by comparing unique emoji.": "如果你不能扫描以上代码,请通过比较唯一的表情符号来验证。", "Verify by comparing unique emoji.": "通过比较唯一的表情符号来验证。", "Verify by emoji": "通过表情符号验证", - "Almost there! Is your other session showing the same shield?": "快完成了!您的另一个会话显示了同样的盾牌吗?", + "Almost there! Is your other session showing the same shield?": "快完成了!你的另一个会话显示了同样的盾牌吗?", "Almost there! Is %(displayName)s showing the same shield?": "快完成了!%(displayName)s 显示了同样的盾牌吗?", "Verify all users in a room to ensure it's secure.": "验证聊天室中所有用户以确保其安全。", "In encrypted rooms, verify all users to ensure it’s secure.": "在加密聊天室中,验证所有用户以确保其安全。", - "You've successfully verified your device!": "您成功验证了您的设备!", - "You've successfully verified %(deviceName)s (%(deviceId)s)!": "您成功验证了 %(deviceName)s (%(deviceId)s)!", - "You've successfully verified %(displayName)s!": "您成功验证了 %(displayName)s!", + "You've successfully verified your device!": "你成功验证了你的设备!", + "You've successfully verified %(deviceName)s (%(deviceId)s)!": "你成功验证了 %(deviceName)s (%(deviceId)s)!", + "You've successfully verified %(displayName)s!": "你成功验证了 %(displayName)s!", "Verified": "已验证", "Got it": "知道了", "Start verification again from the notification.": "请从提示重新开始验证。", "Start verification again from their profile.": "请从对方资料重新开始验证。", "Verification timed out.": "雅正超时。", - "You cancelled verification on your other session.": "您在您别的会话上取消了验证。", + "You cancelled verification on your other session.": "你在你别的会话上取消了验证。", "%(displayName)s cancelled verification.": "%(displayName)s 取消了验证。", - "You cancelled verification.": "您取消了验证。", + "You cancelled verification.": "你取消了验证。", "Verification cancelled": "验证已取消", "Compare emoji": "比较表情符号", "Encryption enabled": "已启用加密", @@ -1877,20 +1877,20 @@ "React": "回应", "Message Actions": "消息操作", "Show image": "显示图像", - "You have ignored this user, so their message is hidden. Show anyways.": "您已忽略该用户,所以其消息已被隐藏。仍然显示。", - "You verified %(name)s": "您验证了 %(name)s", - "You cancelled verifying %(name)s": "您取消了 %(name)s 的验证", + "You have ignored this user, so their message is hidden. Show anyways.": "你已忽略此用户,所以其消息已被隐藏。仍然显示。", + "You verified %(name)s": "你验证了 %(name)s", + "You cancelled verifying %(name)s": "你取消了 %(name)s 的验证", "%(name)s cancelled verifying": "%(name)s 取消了验证", - "You accepted": "您接受了", + "You accepted": "你接受了", "%(name)s accepted": "%(name)s 接受了", - "You declined": "您拒绝了", - "You cancelled": "您取消了", + "You declined": "你拒绝了", + "You cancelled": "你取消了", "%(name)s declined": "%(name)s 拒绝了", "%(name)s cancelled": "%(name)s 取消了", "Accepting …": "正在接受…", "Declining …": "正在拒绝…", "%(name)s wants to verify": "%(name)s 想要验证", - "You sent a verification request": "您发送了一个验证请求", + "You sent a verification request": "你发送了一个验证请求", "Show all": "显示全部", "Reactions": "回应", " reacted with %(content)s": " 回应了 %(content)s", @@ -1917,14 +1917,14 @@ "Quick Reactions": "快速回应", "Cancel search": "取消搜索", "Any of the following data may be shared:": "以下数据之一可能被分享:", - "Your display name": "您的显示名称", - "Your avatar URL": "您的头像链接", - "Your user ID": "您的用户 ID", - "Your theme": "您的主题", + "Your display name": "你的显示名称", + "Your avatar URL": "你的头像链接", + "Your user ID": "你的用户 ID", + "Your theme": "你的主题", "%(brand)s URL": "%(brand)s 的链接", "Room ID": "聊天室 ID", "Widget ID": "挂件 ID", - "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "使用此挂件可能会和 %(widgetDomain)s 及您的集成管理器共享数据 。", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "使用此挂件可能会和 %(widgetDomain)s 及你的集成管理器共享数据 。", "Using this widget may share data with %(widgetDomain)s.": "使用此挂件可能会和 %(widgetDomain)s 共享数据 。", "Widgets do not use message encryption.": "挂件不适用消息加密。", "This widget may use cookies.": "此挂件可能使用 cookie。", @@ -1945,12 +1945,12 @@ "Looks good": "看着不错", "Can't find this server or its room list": "找不到此服务器或其聊天室列表", "All rooms": "所有聊天室", - "Your server": "您的服务器", - "Are you sure you want to remove %(serverName)s": "您确定要移除 %(serverName)s 吗", + "Your server": "你的服务器", + "Are you sure you want to remove %(serverName)s": "你确定要移除 %(serverName)s 吗", "Remove server": "移除服务器", "Matrix": "Matrix", "Add a new server": "添加新服务器", - "Enter the name of a new server you want to explore.": "输入您想探索的新服务器的服务器名。", + "Enter the name of a new server you want to explore.": "输入你想探索的新服务器的服务器名。", "Server name": "服务器名", "Add a new server...": "添加新服务器…", "%(networkName)s rooms": "%(networkName)s 的聊天室", @@ -1958,14 +1958,14 @@ "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "使用一个身份服务器以通过邮箱邀请。使用默认(%(defaultIdentityServerName)s)或在设置中管理。", "Use an identity server to invite by email. Manage in Settings.": "使用一个身份服务器以通过邮箱邀请。在设置中管理。", "Close dialog": "关闭对话框", - "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "请告诉我们哪里出错了,或最好创建一个 GitHub issue 来描述该问题。", - "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "提醒:您的浏览器不被支持,所以您的体验可能不可预料。", + "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "请告诉我们哪里出错了,或最好创建一个 GitHub issue 来描述此问题。", + "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "提醒:你的浏览器不被支持,所以你的体验可能不可预料。", "GitHub issue": "GitHub 上的 issue", "Notes": "提示", - "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "如果有额外的上下文可以帮助我们分析问题,比如您当时在做什么、房间 ID、用户 ID 等等,请将其列于此处。", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "如果有额外的上下文可以帮助我们分析问题,比如你当时在做什么、房间 ID、用户 ID 等等,请将其列于此处。", "Removing…": "正在移除…", "Destroy cross-signing keys?": "销毁交叉签名密钥?", - "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "删除交叉签名密钥是永久的。所有您验证过的人都会看到安全警报。除非您丢失了所有可以交叉签名的设备,否则几乎可以确定您不想这么做。", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "删除交叉签名密钥是永久的。所有你验证过的人都会看到安全警报。除非你丢失了所有可以交叉签名的设备,否则几乎可以确定你不想这么做。", "Clear cross-signing keys": "清楚交叉签名密钥", "Clear all data in this session?": "是否清除此会话中的所有数据?", "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "清除此会话中的所有数据是永久的。加密消息会丢失,除非其密钥已被备份。", @@ -1973,7 +1973,7 @@ "Please enter a name for the room": "请输入聊天室名称", "Set a room address to easily share your room with other people.": "设置一个聊天室地址以轻松地和别人共享您的聊天室。", "This room is private, and can only be joined by invitation.": "此聊天室是私人的,只能通过邀请加入。", - "You can’t disable this later. Bridges & most bots won’t work yet.": "您之后不能禁用此项。桥接和大部分机器人还不能正常工作。", + "You can’t disable this later. Bridges & most bots won’t work yet.": "你之后不能禁用此项。桥接和大部分机器人还不能正常工作。", "Enable end-to-end encryption": "启用端对端加密", "Create a public room": "创建一个公共聊天室", "Create a private room": "创建一个私人聊天室", @@ -1982,28 +1982,28 @@ "Hide advanced": "隐藏高级", "Show advanced": "显示高级", "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "阻止别的 matrix 主服务器上的用户加入此聊天室(此设置之后不能更改!)", - "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "您曾在此会话中使用了一个更新版本的 %(brand)s。要再使用此版本并使用端对端加密,您需要登出再重新登录。", - "Confirm your account deactivation by using Single Sign On to prove your identity.": "通过单点登录证明您的身份并确认停用您的账号。", - "Are you sure you want to deactivate your account? This is irreversible.": "您确定要停用您的账号吗?此操作不可逆。", + "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "你曾在此会话中使用了一个更新版本的 %(brand)s。要再使用此版本并使用端对端加密,你需要登出再重新登录。", + "Confirm your account deactivation by using Single Sign On to prove your identity.": "通过单点登录证明你的身份并确认停用你的账号。", + "Are you sure you want to deactivate your account? This is irreversible.": "你确定要停用你的账号吗?此操作不可逆。", "Confirm account deactivation": "确认账号停用", "There was a problem communicating with the server. Please try again.": "联系服务器时出现问题。请重试。", "Server did not require any authentication": "服务器不要求任何认证", "Server did not return valid authentication information.": "服务器未返回有效认证信息。", "View Servers in Room": "查看聊天室中的服务器", "Verification Requests": "验证请求", - "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "验证此用户会将其会话标记为已信任,与此同时,您的会话也会被此用户标记为已信任。", - "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.": "验证此设备以将其标记为已信任。在收发端对端加密消息时,信任设备可让您与其他用户更加放心。", - "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "验证此设备会将其标记为已信任,与此同时,其他验证了您的用户也会信任此设备。", + "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "验证此用户会将其会话标记为已信任,与此同时,你的会话也会被此用户标记为已信任。", + "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.": "验证此设备以将其标记为已信任。在收发端对端加密消息时,信任设备可让你与其他用户更加放心。", + "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "验证此设备会将其标记为已信任,与此同时,其他验证了你的用户也会信任此设备。", "Integrations are disabled": "集成已禁用", "Enable 'Manage Integrations' in Settings to do this.": "在设置中启用「管理集成」以执行此操作。", "Integrations not allowed": "集成未被允许", - "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "您的 %(brand)s 不允许您使用集成管理器来完成此操作。请联系管理员。", - "To continue, use Single Sign On to prove your identity.": "要继续,请使用单点登录证明您的身份。", + "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "你的 %(brand)s 不允许你使用集成管理器来完成此操作。请联系管理员。", + "To continue, use Single Sign On to prove your identity.": "要继续,请使用单点登录证明你的身份。", "Confirm to continue": "确认以继续", - "Click the button below to confirm your identity.": "点击下方按钮确认您的身份。", + "Click the button below to confirm your identity.": "点击下方按钮确认你的身份。", "Failed to invite the following users to chat: %(csvUsers)s": "邀请以下用户加入聊天失败:%(csvUsers)s", "Something went wrong trying to invite the users.": "尝试邀请用户时出错。", - "We couldn't invite those users. Please check the users you want to invite and try again.": "我们不能邀请这些用户。请检查您想邀请的用户并重试。", + "We couldn't invite those users. Please check the users you want to invite and try again.": "我们不能邀请这些用户。请检查你想邀请的用户并重试。", "Failed to find the following users": "寻找以下用户失败", "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "下列用户可能不存在或无效,因此不能被邀请:%(csvNames)s", "Recent Conversations": "最近对话", @@ -2023,48 +2023,48 @@ "Signature upload success": "签名上传成功", "Signature upload failed": "签名上传失败", "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "如果别的 %(brand)s 版本在别的标签页中仍然开启,请关闭它,因为在同一宿主上同时使用开启了延迟加载和关闭了延迟加载的 %(brand)s 会导致问题。", - "Confirm by comparing the following with the User Settings in your other session:": "通过比较下方内容和您别的会话中的用户设置来确认:", + "Confirm by comparing the following with the User Settings in your other session:": "通过比较下方内容和你别的会话中的用户设置来确认:", "Confirm this user's session by comparing the following with their User Settings:": "通过比较下方内容和对方用户设置来确认此用户会话:", "Session name": "会话名称", "Session key": "会话密钥", - "If they don't match, the security of your communication may be compromised.": "如果它们不匹配,您通讯的安全性可能已受损。", + "If they don't match, the security of your communication may be compromised.": "如果它们不匹配,你通讯的安全性可能已受损。", "Verify session": "验证会话", - "Your homeserver doesn't seem to support this feature.": "您的主服务器似乎不支持此功能。", + "Your homeserver doesn't seem to support this feature.": "你的主服务器似乎不支持此功能。", "Message edits": "消息编辑历史", - "Your account is not secure": "您的账号不安全", - "Your password": "您的密码", + "Your account is not secure": "你的账号不安全", + "Your password": "你的密码", "This session, or the other session": "此会话,或别的会话", - "The internet connection either session is using": "您会话使用的网络连接", + "The internet connection either session is using": "你会话使用的网络连接", "We recommend you change your password and recovery key in Settings immediately": "我们推荐您立刻在设置中更改您的密码和恢复密钥", "New session": "新会话", - "Use this session to verify your new one, granting it access to encrypted messages:": "使用此会话以验证您的新会话,并允许其访问加密信息:", - "If you didn’t sign in to this session, your account may be compromised.": "如果您没有登录进此会话,您的账号可能已受损。", + "Use this session to verify your new one, granting it access to encrypted messages:": "使用此会话以验证你的新会话,并允许其访问加密信息:", + "If you didn’t sign in to this session, your account may be compromised.": "如果你没有登录进此会话,你的账号可能已受损。", "This wasn't me": "这不是我", "Use your account to sign in to the latest version of the app at ": "使用您的账户在 登录此应用的最新版", "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at element.io/get-started.": "您已经登录且一切已就绪,但您也可以在 element.io/get-started 获取此应用在全平台上的最新版。", "Go to Element": "前往 Element", "We’re excited to announce Riot is now Element!": "我们很兴奋地宣布 Riot 现在是 Element 了!", "Learn more at element.io/previously-riot": "访问 element.io/previously-riot 了解更多", - "Please fill why you're reporting.": "请填写您为何做此报告。", - "Report Content to Your Homeserver Administrator": "向您的主服务器管理员举报内容", - "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "举报此消息会将其唯一的「事件 ID」发送给您的主服务器的管理员。如果此聊天室中的消息被加密,您的主服务器管理员将不能阅读消息文本,也不能查看任何文件或图片。", + "Please fill why you're reporting.": "请填写你为何做此报告。", + "Report Content to Your Homeserver Administrator": "向你的主服务器管理员举报内容", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "举报此消息会将其唯一的「事件 ID」发送给你的主服务器的管理员。如果此聊天室中的消息被加密,你的主服务器管理员将不能阅读消息文本,也不能查看任何文件或图片。", "Send report": "发送报告", "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "更新此聊天室需要关闭此聊天室的当前实力并创建一个新的聊天室代替它。为了给聊天室成员最好的体验,我们会:", "Automatically invite users": "自动邀请用户", "Upgrade private room": "更新私人聊天室", "Upgrade public room": "更新公共聊天室", "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "更新聊天室是高级操作,通常建议在聊天室由于错误、缺失功能或安全漏洞而不稳定时使用。", - "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "通常这只影响聊天室在服务器上的处理方式。如果您对您的 %(brand)s 有问题,请报告一个错误。", - "You'll upgrade this room from to .": "您将把此聊天室从 升级至 。", + "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "通常这只影响聊天室在服务器上的处理方式。如果你对你的 %(brand)s 有问题,请报告一个错误。", + "You'll upgrade this room from to .": "你将把此聊天室从 升级至 。", "You're all caught up.": "全数阅毕。", "Server isn't responding": "服务器未响应", - "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "您的服务器未响应您的一些请求。下方是一些最可能的原因。", + "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "你的服务器未响应你的一些请求。下方是一些最可能的原因。", "The server (%(serverName)s) took too long to respond.": "服务器(%(serverName)s)花了太长时间响应。", - "Your firewall or anti-virus is blocking the request.": "您的防火墙或防病毒软件阻止了该请求。", - "A browser extension is preventing the request.": "一个浏览器扩展阻止了该请求。", - "The server is offline.": "该服务器为离线状态。", - "The server has denied your request.": "该服务器拒绝了您的请求。", - "Your area is experiencing difficulties connecting to the internet.": "您的区域难以连接上互联网。", + "Your firewall or anti-virus is blocking the request.": "你的防火墙或防病毒软件阻止了此请求。", + "A browser extension is preventing the request.": "一个浏览器扩展阻止了此请求。", + "The server is offline.": "此服务器为离线状态。", + "The server has denied your request.": "此服务器拒绝了你的请求。", + "Your area is experiencing difficulties connecting to the internet.": "你的区域难以连接上互联网。", "A connection error occurred while trying to contact the server.": "尝试联系服务器时出现连接错误。", "Recent changes that have not yet been received": "尚未被接受的最近更改", "Sign out and remove encryption keys?": "登出并删除加密密钥?", @@ -2073,13 +2073,13 @@ "To help us prevent this in future, please send us logs.": "要帮助我们防止其以后发生,请给我们发送日志。", "Missing session data": "缺失会话数据", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "一些会话数据,包括加密消息密钥,已缺失。要修复此问题,登出并重新登录,然后从备份恢复密钥。", - "Your browser likely removed this data when running low on disk space.": "您的浏览器可能在磁盘空间不足时删除了此数据。", + "Your browser likely removed this data when running low on disk space.": "你的浏览器可能在磁盘空间不足时删除了此数据。", "Integration Manager": "集成管理器", "Find others by phone or email": "通过电话或邮箱寻找别人", "Be found by phone or email": "通过电话或邮箱被寻找", "Use bots, bridges, widgets and sticker packs": "使用机器人、桥接、挂件和贴图集", "Terms of Service": "服务协议", - "To continue you need to accept the terms of this service.": "要继续,您需要接受此服务协议。", + "To continue you need to accept the terms of this service.": "要继续,你需要接受此服务协议。", "Service": "服务", "Summary": "总结", "Document": "文档", @@ -2101,9 +2101,9 @@ "Invalid Recovery Key": "无效的恢复密钥", "Security Phrase": "安全密码", "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "无法访问秘密存储。请确认您输入了正确的恢复密码。", - "Enter your Security Phrase or to continue.": "输入您的安全密码或以继续。", + "Enter your Security Phrase or to continue.": "输入你的安全密码或以继续。", "Security Key": "安全密钥", - "Use your Security Key to continue.": "使用您的安全密钥以继续。", + "Use your Security Key to continue.": "使用你的安全密钥以继续。", "Restoring keys from backup": "从备份恢复密钥", "Fetching keys from server...": "正在从服务器获取密钥...", "%(completed)s of %(total)s keys restored": "%(total)s 个密钥中之 %(completed)s 个已恢复", @@ -2115,7 +2115,7 @@ "Successfully restored %(sessionCount)s keys": "成功恢复了 %(sessionCount)s 个密钥", "Enter recovery passphrase": "输入恢复密码", "Enter recovery key": "输入恢复密钥", - "Warning: You should only set up key backup from a trusted computer.": "警告:您应该只从信任的计算机设置密钥备份。", + "Warning: You should only set up key backup from a trusted computer.": "警告:你应此只从信任的计算机设置密钥备份。", "If you've forgotten your recovery key you can ": "如果您忘记了恢复密钥,您可以", "Address (optional)": "地址(可选)", "Resend edit": "重新发送编辑", @@ -2130,19 +2130,19 @@ "Remove for me": "为我删除", "User Status": "用户状态", "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.": "您可以使用自定义服务器选项以使用不同的主服务器链接登录至别的 Matrix 服务器。这允许您通过不同的主服务器上的现存 Matrix 账户使用 %(brand)s。", - "Confirm your identity by entering your account password below.": "在下方输入账号密码以确认您的身份。", - "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "在主服务器配置中缺少验证码公钥。请将此报告给您的主服务器管理员。", + "Confirm your identity by entering your account password below.": "在下方输入账号密码以确认你的身份。", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "在主服务器配置中缺少验证码公钥。请将此报告给你的主服务器管理员。", "Unable to validate homeserver/identity server": "无法验证主服务器/身份服务器", "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of element.io.": "输入您的 Element Matrix Services 主服务器的地址。它可能使用您自己的域名,也可能是 element.io 的子域名。", "Enter password": "输入密码", "Nice, strong password!": "不错,是个强密码!", "Password is allowed, but unsafe": "密码允许但不安全", "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "没有配置身份服务器因此您不能添加邮件地址以在将来重置您的密码。", - "Use an email address to recover your account": "使用邮件地址恢复您的账号", + "Use an email address to recover your account": "使用邮件地址恢复你的账号", "Enter email address (required on this homeserver)": "输入邮件地址(此主服务器上必须)", "Doesn't look like a valid email address": "看起来不像有效的邮件地址", "Passwords don't match": "密码不匹配", - "Other users can invite you to rooms using your contact details": "别的用户可以使用您的联系人信息邀请您加入聊天室", + "Other users can invite you to rooms using your contact details": "别的用户可以使用你的联系人信息邀请你加入聊天室", "Enter phone number (required on this homeserver)": "输入电话号码(此主服务器上必须)", "Doesn't look like a valid phone number": "看着不像一个有效的电话号码", "Use lowercase letters, numbers, dashes and underscores only": "仅使用小写字母,数字,横杠和下划线", @@ -2156,7 +2156,7 @@ "Sign in with SSO": "使用单点登录", "No files visible in this room": "此聊天室中没有可见文件", "Welcome to %(appName)s": "欢迎来到 %(appName)s", - "Liberate your communication": "解放您的交流", + "Liberate your communication": "解放你的交流", "Send a Direct Message": "发送私聊", "Explore Public Rooms": "探索公共聊天室", "Create a Group Chat": "创建一个群聊", @@ -2171,7 +2171,7 @@ "View": "查看", "Find a room…": "寻找聊天室…", "Find a room… (e.g. %(exampleRoom)s)": "寻找聊天室... (例如 %(exampleRoom)s)", - "If you can't find the room you're looking for, ask for an invite or Create a new room.": "如果您不能找到您所寻找的聊天室,请索要一个邀请或创建新聊天室。", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "如果你不能找到你所寻找的聊天室,请索要一个邀请或创建新聊天室。", "Search rooms": "搜索聊天室", "Switch to light mode": "切换到浅色模式", "Switch to dark mode": "切换到深色模式", @@ -2183,7 +2183,7 @@ "Session verified": "会话已验证", "Your Matrix account on ": "您在 上的 Matrix 账户", "No identity server is configured: add one in server settings to reset your password.": "没有配置身份服务器:在服务器设置中添加一个以重设您的密码。", - "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "您已经登出了所有会话,并将不会收到推送通知。要重新启用通知,请在每个设备上重新登录。", + "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "你已经登出了所有会话,并将不会收到推送通知。要重新启用通知,请在每个设备上重新登录。", "Failed to get autodiscovery configuration from server": "从服务器获取自动发现配置时失败", "Invalid base_url for m.homeserver": "m.homeserver 的 base_url 无效", "Homeserver URL does not appear to be a valid Matrix homeserver": "主服务器链接不像是有效的 Matrix 主服务器", @@ -2192,11 +2192,11 @@ "This account has been deactivated.": "此账号已被停用。", "Syncing...": "正在同步...", "Signing In...": "正在登录...", - "If you've joined lots of rooms, this might take a while": "如果您加入了很多聊天室,可能会消耗一些时间", - "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "您的新账号(%(newAccountId)s)已注册,但您已经登录了一个不同的账号(%(loggedInUserId)s)。", + "If you've joined lots of rooms, this might take a while": "如果你加入了很多聊天室,可能会消耗一些时间", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "你的新账号(%(newAccountId)s)已注册,但你已经登录了一个不同的账号(%(loggedInUserId)s)。", "Continue with previous account": "用之前的账号继续", - "Log in to your new account.": "登录到您的新账号。", - "You can now close this window or log in to your new account.": "您现在可以关闭此窗口或登录到您的新账号。", + "Log in to your new account.": "登录到你的新账号。", + "You can now close this window or log in to your new account.": "你现在可以关闭此窗口或登录到你的新账号。", "Registration Successful": "注册成功", "Use Recovery Key or Passphrase": "使用恢复密钥或密码", "Use Recovery Key": "使用恢复密钥", @@ -2206,21 +2206,21 @@ "%(brand)s iOS": "%(brand)s iOS", "%(brand)s X for Android": "%(brand)s X for Android", "or another cross-signing capable Matrix client": "或者别的可以交叉签名的 Matrix 客户端", - "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "您的新会话现已被验证。它可以访问您的加密消息,别的用户也会视其为受信任的。", - "Your new session is now verified. Other users will see it as trusted.": "您的新会话现已被验证。别的用户会视其为受信任的。", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "你的新会话现已被验证。它可以访问你的加密消息,别的用户也会视其为受信任的。", + "Your new session is now verified. Other users will see it as trusted.": "你的新会话现已被验证。别的用户会视其为受信任的。", "Without completing security on this session, it won’t have access to encrypted messages.": "若不在此会话中完成安全验证,它便不能访问加密消息。", "Failed to re-authenticate due to a homeserver problem": "由于主服务器的问题,重新认证失败", "Failed to re-authenticate": "重新认证失败", - "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "重新获得访问您账号的权限,并恢复存储在此会话中的加密密钥。没有这些密钥,您将不能在任何会话中阅读您的所有安全消息。", - "Enter your password to sign in and regain access to your account.": "输入您的密码以登录并重新获取访问您账号的权限。", - "Forgotten your password?": "忘记您的密码了吗?", - "Sign in and regain access to your account.": "请登录以重新获取访问您账号的权限。", - "You cannot sign in to your account. Please contact your homeserver admin for more information.": "您不能登录进您的账号。请联系您的主服务器管理员以获取更多信息。", - "You're signed out": "您已登出", + "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "重新获得访问你账号的权限,并恢复存储在此会话中的加密密钥。没有这些密钥,你将不能在任何会话中阅读你的所有安全消息。", + "Enter your password to sign in and regain access to your account.": "输入你的密码以登录并重新获取访问你账号的权限。", + "Forgotten your password?": "忘记你的密码了吗?", + "Sign in and regain access to your account.": "请登录以重新获取访问你账号的权限。", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "你不能登录进你的账号。请联系你的主服务器管理员以获取更多信息。", + "You're signed out": "你已登出", "Clear personal data": "清除个人信息", - "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.": "警告:您的个人信息(包括加密密钥)仍存储于此会话中。如果您不用再使用此会话或想登录进另一个账号,请清除它。", + "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.": "警告:你的个人信息(包括加密密钥)仍存储于此会话中。如果你不用再使用此会话或想登录进另一个账号,请清除它。", "Command Autocomplete": "命令自动补全", - "Community Autocomplete": "社区自动补全", + "Community Autocomplete": "社群自动补全", "DuckDuckGo Results": "DuckDuckGo 结果", "Emoji Autocomplete": "表情符号自动补全", "Notification Autocomplete": "通知自动补全", @@ -2228,32 +2228,32 @@ "User Autocomplete": "用户自动补全", "Confirm encryption setup": "确认加密设置", "Click the button below to confirm setting up encryption.": "点击下方按钮以确认设置加密。", - "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "通过在您的服务器上备份加密密钥来防止丢失您对加密消息和数据的访问权。", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "通过在你的服务器上备份加密密钥来防止丢失你对加密消息和数据的访问权。", "Generate a Security Key": "生成一个安全密钥", - "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "我们会生成一个安全密钥以便让您存储在安全的地方,比如密码管理器或保险箱里。", + "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "我们会生成一个安全密钥以便让你存储在安全的地方,比如密码管理器或保险箱里。", "Enter a Security Phrase": "输入一个安全密码", - "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "使用一个只有您知道的密码,您也可以保存安全密钥以供备份使用。", - "Enter your account password to confirm the upgrade:": "输入您的账号密码以确认更新:", - "Restore your key backup to upgrade your encryption": "恢复您的密钥备份以更新您的加密方式", + "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "使用一个只有你知道的密码,你也可以保存安全密钥以供备份使用。", + "Enter your account password to confirm the upgrade:": "输入你的账号密码以确认更新:", + "Restore your key backup to upgrade your encryption": "恢复你的密钥备份以更新你的加密方式", "Restore": "恢复", - "You'll need to authenticate with the server to confirm the upgrade.": "您需要和服务器进行认证以确认更新。", + "You'll need to authenticate with the server to confirm the upgrade.": "你需要和服务器进行认证以确认更新。", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "更新此会话以允许其验证其他会话、允许其他会话访问加密消息,并将它们对别的用户标记为已信任。", - "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "输入一个只有您知道的安全密码,它将被用来保护您的数据。为了安全,您不应该复用您的账号密码。", + "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "输入一个只有你知道的安全密码,它将被用来保护你的数据。为了安全,你不应该复用你的账号密码。", "Enter a recovery passphrase": "输入一个恢复密码", "Great! This recovery passphrase looks strong enough.": "棒!这个恢复密码看着够强。", "Use a different passphrase?": "使用不同的密语?", "Enter your recovery passphrase a second time to confirm it.": "再次输入您的恢复密语以确认。", "Confirm your recovery passphrase": "确认您的恢复密语", - "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "将您的安全密钥存储在安全的地方,像是密码管理器或保险箱里,它将被用来保护您的加密数据。", + "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "将你的安全密钥存储在安全的地方,像是密码管理器或保险箱里,它将被用来保护你的加密数据。", "Copy": "复制", "Unable to query secret storage status": "无法查询秘密存储状态", - "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "如果您现在取消,您可能会丢失加密的消息和数据,如果您丢失了登录信息的话。", - "You can also set up Secure Backup & manage your keys in Settings.": "您也可以在设置中设置安全备份并管理您的密钥。", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "如果你现在取消,你可能会丢失加密的消息和数据,如果你丢失了登录信息的话。", + "You can also set up Secure Backup & manage your keys in Settings.": "你也可以在设置中设置安全备份并管理你的密钥。", "Set up Secure backup": "设置安全备份", - "Upgrade your encryption": "更新您的加密方法", + "Upgrade your encryption": "更新你的加密方法", "Set a Security Phrase": "设置一个安全密码", "Confirm Security Phrase": "确认安全密码", - "Save your Security Key": "保存您的安全密钥", + "Save your Security Key": "保存你的安全密钥", "Unable to set up secret storage": "无法设置秘密存储", "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "我们会在服务器上存储一份您的密钥的加密副本。用恢复密码来保护您的备份。", "Set up with a recovery key": "用恢复密钥设置", @@ -2264,13 +2264,13 @@ "Your recovery key": "您的恢复密钥", "Your recovery key has been copied to your clipboard, paste it to:": "您的恢复密钥已被复制到您的剪贴板,将其粘贴至:", "Your recovery key is in your Downloads folder.": "您的恢复密钥在您的下载文件夹里。", - "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "若不设置安全消息恢复,您如果登出或使用另一个会话,则将不能恢复您的加密消息历史。", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "若不设置安全消息恢复,你如果登出或使用另一个会话,则将不能恢复你的加密消息历史。", "Secure your backup with a recovery passphrase": "用恢复密码保护您的备份", "Make a copy of your recovery key": "制作一份您的恢复密钥的副本", "Create key backup": "创建密钥备份", "This session is encrypting history using the new recovery method.": "此会话正在使用新的恢复方法加密历史。", "This session has detected that your recovery passphrase and key for Secure Messages have been removed.": "此会话检测到您的恢复密码和安全消息的密钥已被移除。", - "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "如果您出于意外这样做了,您可以在此会话上设置安全消息,以使用新的加密方式重新加密此会话的消息历史。", + "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "如果你出于意外这样做了,你可以在此会话上设置安全消息,以使用新的加密方式重新加密此会话的消息历史。", "If disabled, messages from encrypted rooms won't appear in search results.": "如果被禁用,加密聊天室内的消息不会显示在搜索结果中。", "Disable": "禁用", "Not currently indexing messages for any room.": "现在没有为任何聊天室索引消息。", @@ -2329,7 +2329,7 @@ "Securely cache encrypted messages locally for them to appear in search results, using ": "在本地安全缓存已加密消息以使其出现在搜索结果中,使用 ", " to store messages from ": " 存储来自 ", "rooms.": "聊天室的消息。", - "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "此会话未备份您的密钥,但您已有备份,您可以继续并从中恢复和向其添加。", + "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "此会话未备份你的密钥,但如果你已有现存备份,你可以继续并从中恢复和向其添加。", "Invalid theme schema.": "无效主题方案。", "Read Marker lifetime (ms)": "已读标记生存期 (ms)", "Read Marker off-screen lifetime (ms)": "已读标记屏幕外生存期 (ms)", @@ -2338,11 +2338,11 @@ "Unable to revoke sharing for phone number": "无法撤销电话号码共享", "Mod": "管理员", "Explore public rooms": "探索公共聊天室", - "Can't see what you’re looking for?": "看不到您要找的吗?", + "Can't see what you’re looking for?": "看不到你要找的吗?", "Explore all public rooms": "探索所有公共聊天室", "%(count)s results|other": "%(count)s 个结果", - "You can only join it with a working invite.": "您只能通过有效邀请加入。", - "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "您尝试验证的会话不支持 %(brand)s 支持的扫描二维码或表情符号验证。尝试使用其他客户端。", + "You can only join it with a working invite.": "你只能通过有效邀请加入。", + "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "你尝试验证的会话不支持 %(brand)s 支持的扫描二维码或表情符号验证。尝试使用其他客户端。", "Language Dropdown": "语言下拉菜单", "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s 未做更改 %(count)s 次", "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s 未做更改", @@ -2380,7 +2380,7 @@ "Group call started by %(senderName)s": "%(senderName)s 发起的群通话", "Group call ended by %(senderName)s": "%(senderName)s 结束了群通话", "Unknown App": "未知应用", - "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "社区 v2 原型。需要兼容的主服务器。高度实验性 - 谨慎使用。", + "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "社群 v2 原型。需要兼容的主服务器。高度实验性 - 谨慎使用。", "Cross-signing is ready for use.": "交叉签名已可用。", "Cross-signing is not set up.": "未设置交叉签名。", "Backup version:": "备份版本:", @@ -2396,7 +2396,7 @@ "not ready": "尚未就绪", "Secure Backup": "安全备份", "Privacy": "隐私", - "Explore community rooms": "探索社区聊天室", + "Explore community rooms": "探索社群聊天室", "%(count)s results|one": "%(count)s 个结果", "Room Info": "聊天室信息", "No other application is using the webcam": "没有其他应用程序正在使用摄像头", @@ -2406,7 +2406,7 @@ "Unable to access webcam / microphone": "无法访问摄像头/麦克风", "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "呼叫失败,因为无法访问任何麦克风。 检查是否已插入麦克风并正确设置。", "Unable to access microphone": "无法使用麦克风", - "The call was answered on another device.": "在另一台设备上应答了该通话。", + "The call was answered on another device.": "在另一台设备上应答了此通话。", "The call could not be established": "无法建立通话", "The other party declined the call.": "对方拒绝了通话。", "Call Declined": "通话被拒绝", @@ -2471,18 +2471,18 @@ "Afghanistan": "阿富汗", "United States": "美国", "United Kingdom": "英国", - "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "您的主服务器已拒绝您的登入尝试。请重试。如果此情况持续发生,请联系您的主服务器管理员。", - "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "无法访问您的主服务器,因而无法登入。请重试。如果此情况持续发生,请联系您的主服务器管理员。", + "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "你的主服务器已拒绝你的登入尝试。请重试。如果此情况持续发生,请联系你的主服务器管理员。", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "无法访问你的主服务器,因而无法登入。请重试。如果此情况持续发生,请联系你的主服务器管理员。", "Try again": "重试", - "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "我们已要求浏览器记住您使用的主服务器,但不幸的是您的浏览器已忘记。请前往登录页面重试。", - "We couldn't log you in": "我们无法使您登入", + "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "我们已要求浏览器记住你使用的主服务器,但不幸的是你的浏览器已忘记。请前往登录页面重试。", + "We couldn't log you in": "我们无法使你登入", "This will end the conference for everyone. Continue?": "这将结束所有人的会议。是否继续?", "End conference": "结束会议", - "You've reached the maximum number of simultaneous calls.": "您已达到并行呼叫最大数量。", + "You've reached the maximum number of simultaneous calls.": "你已达到并行呼叫最大数量。", "Too Many Calls": "太多呼叫", "Call failed because webcam or microphone could not be accessed. Check that:": "通话失败,因为无法访问网络摄像头或麦克风。请检查:", "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "呼叫失败,因为无法访问任何麦克风。 检查是否已插入麦克风并正确设置。", - "Answered Elsewhere": "在其他地方已回答", + "Answered Elsewhere": "已在其他地方回答", "Use the + to make a new room or explore existing ones below": "使用 + 创建新的聊天室或通过下面列出的方式探索已有聊天室", "Start a new chat": "开始新会话", "Room settings": "聊天室设置", @@ -2497,10 +2497,10 @@ "Creating...": "创建中……", "You can change these at any point.": "您可随时更改这些。", "Give it a photo, name and description to help you identify it.": "为它添加一张照片、姓名与描述来帮助您辨认它。", - "Your private space": "您的私有空间", - "Your public space": "您的公共空间", - "You can change this later": "您可稍后更改此项", - "Invite only, best for yourself or teams": "仅邀请,适合您自己或团队", + "Your private space": "你的私有空间", + "Your public space": "你的公共空间", + "You can change this later": "你可稍后更改此项", + "Invite only, best for yourself or teams": "仅邀请,适合你自己或团队", "Private": "私有", "Public": "公共", "Delete": "删除", @@ -2511,8 +2511,8 @@ "Voice Call": "语音通话", "Video Call": "视频通话", "%(peerName)s held the call": "%(peerName)s 挂起了通话", - "You held the call Resume": "您挂起了通话 恢复", - "You held the call Switch": "您挂起了通话 切换", + "You held the call Resume": "你挂起了通话 恢复", + "You held the call Switch": "你挂起了通话 切换", "Takes the call in the current room off hold": "解除挂起当前聊天室的通话", "Places the call in the current room on hold": "挂起当前聊天室的通话", "Show chat effects (animations when receiving e.g. confetti)": "显示聊天特效(如收到五彩纸屑时的动画效果)", @@ -2526,7 +2526,7 @@ "Show stickers button": "显示贴纸按钮", "Render LaTeX maths in messages": "在信息中渲染 LaTeX 数学", "%(senderName)s ended the call": "%(senderName)s 结束了通话", - "You ended the call": "您结束了通话", + "You ended the call": "你结束了通话", "This homeserver has been blocked by it's administrator.": "此 homeserver 已被其管理员屏蔽。", "Use app": "使用 app", "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element 网页版在移动设备上仍处于试验阶段。使用免费的原生 app 以获得更好的体验与最新的功能。", @@ -2534,7 +2534,7 @@ "Enable desktop notifications": "开启桌面通知", "Don't miss a reply": "不要错过任何回复", "This homeserver has been blocked by its administrator.": "此 homeserver 已被其管理员屏蔽。", - "Send stickers to this room as you": "以您的身份发送贴纸到此聊天室", + "Send stickers to this room as you": "以你的身份发送贴纸到此聊天室", "Change the avatar of your active room": "更改活跃聊天室的头像", "Change the avatar of this room": "更改当前聊天室的头像", "Change the name of your active room": "更改活跃聊天室的名称", @@ -2543,10 +2543,10 @@ "Change the topic of this room": "更改当前聊天室的话题", "Change which room, message, or user you're viewing": "更改当前正在查看哪个聊天室、消息或用户", "Change which room you're viewing": "更改当前正在查看哪个聊天室", - "Send stickers into your active room": "发送贴纸到您的活跃聊天室", + "Send stickers into your active room": "发送贴纸到你的活跃聊天室", "Send stickers into this room": "发送贴纸到此聊天室", - "Remain on your screen while running": "运行时始终保留在您的屏幕上", - "Remain on your screen when viewing another room, when running": "运行时始终保留在您的屏幕上,即使您在浏览其它聊天室", + "Remain on your screen while running": "运行时始终保留在你的屏幕上", + "Remain on your screen when viewing another room, when running": "运行时始终保留在你的屏幕上,即使你在浏览其它聊天室", "%(senderName)s declined the call.": "%(senderName)s 拒绝了通话。", "(an error occurred)": "(发生了一个错误)", "(their device couldn't start the camera / microphone)": "(对方的设备无法开启摄像头/麦克风)", @@ -2554,12 +2554,12 @@ "Converts the DM to a room": "将此私聊会话转化为聊天室会话", "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "在纯文本消息开头添加 ┬──┬ ノ( ゜-゜ノ)", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "在纯文本消息开头添加 (╯°□°)╯︵ ┻━┻", - "You're already in a call with this person.": "您与此人已处在通话中。", + "You're already in a call with this person.": "你正在与其通话中。", "Already in call": "已在通话中", "Navigate composer history": "浏览编辑区历史", "Go to Home View": "转到主视图", "Search (must be enabled)": "搜索(必须启用)", - "Your Security Key": "您的安全密钥", + "Your Security Key": "你的安全密钥", "Use Security Key": "使用安全密钥", "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s 或 %(usernamePassword)s", "User settings": "用户设置", @@ -2575,7 +2575,7 @@ "Remove from Space": "从空间中移除", "Undo": "撤销", "Welcome %(name)s": "欢迎 %(name)s", - "Create community": "创建社区", + "Create community": "创建社群", "Forgot password?": "忘记密码?", "Enter Security Key": "输入安全密钥", "Invalid Security Key": "安全密钥无效", @@ -2603,7 +2603,7 @@ "Value": "值", "Setting ID": "设置 ID", "Enter name": "输入名称", - "Community ID: +:%(domain)s": "社区 ID:+:%(domain)s", + "Community ID: +:%(domain)s": "社群 ID:+:%(domain)s", "Reason (optional)": "理由(可选)", "Show": "显示", "Apply": "应用", @@ -2634,7 +2634,7 @@ "Send message": "发送消息", "Invite to this space": "邀请至此空间", "Your message was sent": "消息已发送", - "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "请使用您的账号数据备份加密密钥,以免您无法访问您的会话。密钥将通过一个唯一的安全密钥进行保护。", + "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "请使用你的账号数据备份加密密钥,以免你无法访问你的会话。密钥将通过一个唯一的安全密钥进行保护。", "Spell check dictionaries": "拼写检查字典", "Failed to save your profile": "个人资料保存失败", "The operation could not be completed": "操作无法完成", @@ -2654,11 +2654,11 @@ "Sends the given message with fireworks": "附加烟火发送", "sends fireworks": "发送烟火", "Offline encrypted messaging using dehydrated devices": "需要离线设备(dehydrated devices)的加密消息离线传递", - "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "正在开发的空间功能的原型。与社区、社区 V2 和自定义标签功能不兼容。需要主服务器兼容才能使用某些功能。", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "正在开发的空间功能的原型。与社群、社群 V2 和自定义标签功能不兼容。需要主服务器兼容才能使用某些功能。", "The %(capability)s capability": "%(capability)s 容量", "%(senderName)s has updated the widget layout": "%(senderName)s 已更新挂件布局", "Support": "支持", - "Your server does not support showing space hierarchies.": "您的服务器不支持显示空间层次结构。", + "Your server does not support showing space hierarchies.": "你的服务器不支持显示空间层次结构。", "This version of %(brand)s does not support searching encrypted messages": "当前版本的 %(brand)s 不支持搜索加密消息", "This version of %(brand)s does not support viewing some encrypted files": "当前版本的 %(brand)s 不支持查看某些加密文件", "Effects": "效果", @@ -2760,7 +2760,7 @@ "Explore rooms in %(communityName)s": "在 %(communityName)s 中探索聊天室", "%(count)s messages deleted.|one": "已删除 %(count)s 条消息。", "%(count)s messages deleted.|other": "已删除 %(count)s 条消息。", - "Cannot create rooms in this community": "无法在此社区中创建聊天室", + "Cannot create rooms in this community": "无法在此社群中创建聊天室", "Upgrade to %(hostSignupBrand)s": "升级至 %(hostSignupBrand)s", "Enter phone number": "输入电话号码", "Enter email address": "输入邮箱地址", @@ -2769,13 +2769,13 @@ "Revoke permissions": "撤销权限", "Take a picture": "拍照", "Enter Security Phrase": "输入安全密语", - "Allow this widget to verify your identity": "允许此挂件验证您的身份", + "Allow this widget to verify your identity": "允许此挂件验证你的身份", "Decline All": "全部拒绝", "Approve": "批准", "This widget would like to:": "此挂件想要:", "Approve widget permissions": "批准挂件权限", "Failed to save space settings.": "空间设置保存失败。", - "Sign into your homeserver": "登录您的主服务器", + "Sign into your homeserver": "登录你的主服务器", "Unable to validate homeserver": "无法验证主服务器", "Invalid URL": "URL 无效", "Modal Widget": "模态框挂件(Modal Widget)", @@ -2793,24 +2793,24 @@ "Widgets": "挂件", "This is the start of .": "这里是 的开始。", "Add a photo, so people can easily spot your room.": "添加图片,让人们一眼就能看到你的聊天室。", - "You can change these anytime.": "您随时可以更改它们。", - "Add some details to help people recognise it.": "添加一些细节,以便人们辨识你的社区。", - "Open space for anyone, best for communities": "适合每一个人的开放空间,社区的理想选择", + "You can change these anytime.": "你随时可以更改它们。", + "Add some details to help people recognise it.": "添加一些细节,以便人们辨识你的社群。", + "Open space for anyone, best for communities": "适合每一个人的开放空间,社群的理想选择", "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "空间是为房间和人员分组的新方法。要加入现有的空间,您需要被邀请。", "From %(deviceName)s (%(deviceId)s) at %(ip)s": "来自 %(deviceName)s(%(deviceId)s)于 %(ip)s", "New version of %(brand)s is available": "%(brand)s 有新版本可用", - "You have unverified logins": "您有未验证的登录", + "You have unverified logins": "你有未验证的登录", "You should know": "你应当知道", "Learn more in our , and .": "请通过我们的了解更多信息。", - "Failed to connect to your homeserver. Please close this dialog and try again.": "无法连接至您的主服务器。请关闭此对话框并再试一次。", - "Are you sure you wish to abort creation of the host? The process cannot be continued.": "您确定要放弃创建主机吗?被放弃的创建流程将无法再继续。", + "Failed to connect to your homeserver. Please close this dialog and try again.": "无法连接至你的主服务器。请关闭此对话框并再试一次。", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "你确定要放弃创建主机吗?被放弃的创建流程将无法再继续。", "Confirm abort of host creation": "确定放弃创建主机", "Please view existing bugs on Github first. No match? Start a new one.": "请先查找一下 Github 上已有的问题,以免重复。找不到重复问题?发起一个吧。", - "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "专业建议:如果您要发起新问题,请一并提交调试日志,以便我们找出问题根源。", + "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "专业建议:如果你要发起新问题,请一并提交调试日志,以便我们找出问题根源。", "There are two ways you can provide feedback and help us improve %(brand)s.": "有两种方式可以提供反馈,并帮助我们改进 %(brand)s。", "Please go into as much detail as you like, so we can track down the problem.": "请按照你的意愿,尽可能详细地描述问题,以便我们找出问题根源。", - "Tell us below how you feel about %(brand)s so far.": "请在下面告诉我们直到目前为止您使用 %(brand)s 的感受。", - "There was an error updating your community. The server is unable to process your request.": "更新你的社区时出现错误。服务器无法处理你的请求。", + "Tell us below how you feel about %(brand)s so far.": "请在下面告诉我们直到目前为止你使用 %(brand)s 的感受。", + "There was an error updating your community. The server is unable to process your request.": "更新你的社群时出现错误。服务器无法处理你的请求。", "Values at explicit levels": "各层级的值", "Values at explicit levels:": "各层级的值:", "Values at explicit levels in this room": "此聊天室中各层级的值", @@ -2824,12 +2824,12 @@ "Value in this room": "此聊天室中的值", "Settings Explorer": "设置浏览器", "with state key %(stateKey)s": "附带有状态键(state key)%(stateKey)s", - "Your server requires encryption to be enabled in private rooms.": "您的服务器要求私人房间启用加密。", - "An image will help people identify your community.": "图片可以让人们辨识您的社区。", - "What's the name of your community or team?": "你的社区或者团队的名称是什么?", - "You can change this later if needed.": "如果需要,您可以稍后更改。", - "Use this when referencing your community to others. The community ID cannot be changed.": "在将您的社区推荐给其他人时使用此 ID,社区 ID 不能更改。", - "There was an error creating your community. The name may be taken or the server is unable to process your request.": "创建社区时发生错误。名称可能已被使用,或者服务器无法处理您的请求。", + "Your server requires encryption to be enabled in private rooms.": "你的服务器要求私人房间启用加密。", + "An image will help people identify your community.": "图片可以让人们辨识你的社群。", + "What's the name of your community or team?": "你的社群或者团队的名称是什么?", + "You can change this later if needed.": "如果需要,你可以稍后更改。", + "Use this when referencing your community to others. The community ID cannot be changed.": "在将你的社群推荐给其他人时使用此 ID,社群 ID 不能更改。", + "There was an error creating your community. The name may be taken or the server is unable to process your request.": "创建社群时发生错误。名称可能已被使用,或者服务器无法处理你的请求。", "Invite people to join %(communityName)s": "邀请人们加入 %(communityName)s", "Send %(count)s invites|one": "发送 %(count)s 个邀请", "Send %(count)s invites|other": "发送 %(count)s 个邀请", @@ -2864,7 +2864,7 @@ "Add comment": "添加备注", "Rate %(brand)s": "评价 %(brand)s", "Feedback sent": "反馈已发送", - "Update community": "更新社区", + "Update community": "更新社群", "Failed to save settings": "设置保存失败", "Create a room in %(communityName)s": "在 %(communityName)s 中创建聊天室", "Add image (optional)": "添加图片(可选)", @@ -2879,7 +2879,7 @@ "Invite with email or username": "使用邮箱或者用户名邀请", "Invite people": "邀请人们", "Update %(brand)s": "更新 %(brand)s", - "Check your devices": "检查您的设备", + "Check your devices": "检查你的设备", "Zimbabwe": "津巴布韦", "Zambia": "赞比亚", "Western Sahara": "西撒哈拉", @@ -2960,16 +2960,16 @@ "Ignored attempt to disable encryption": "已忽略禁用加密的尝试", "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "此聊天室中的消息已被端对端加密。当人们加入,你可以点击他们的头像,在他们的资料中验证他们。", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "此处的消息已被端对端加密。请点击对方头像,在其资料中验证 %(displayName)s。", - "Secure your backup with a Security Phrase": "使用安全密语保护您的备份", - "Confirm your Security Phrase": "确认您的安全密语", + "Secure your backup with a Security Phrase": "使用安全密语保护你的备份", + "Confirm your Security Phrase": "确认你的安全密语", "Verify with another session": "使用另一个会话验证", "Use Security Key or Phrase": "使用安全密钥或密语", "Continue with %(ssoButtons)s": "使用 %(ssoButtons)s 继续", "There was a problem communicating with the homeserver, please try again later.": "与主服务器通讯时出现问题,请稍后再试。", "Decrypted event source": "解密事件源码", "Original event source": "原始事件源码", - "Community and user menu": "社区与用户菜单", - "Community settings": "社区设置", + "Community and user menu": "社群与用户菜单", + "Community settings": "社群设置", "Invite by username": "按照用户名邀请", "Inviting...": "正在邀请…", "Welcome to ": "欢迎来到 ", @@ -3013,5 +3013,266 @@ "Guyana": "圭亚那", "Guinea-Bissau": "几内亚比绍", "Guinea": "几内亚", - "Guernsey": "根西岛" + "Guernsey": "根西岛", + "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "你可以启用此选项如果此聊天室将仅用于你的主服务器上的内部团队协作。此选项之后无法更改。", + "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "无法访问秘密存储。请确认你输入了正确的恢复密码。", + "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "无法使用此安全密钥解密备份:请检查你输入的安全密钥是否正确。", + "sends space invaders": "发送空间入侵者", + "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "此会话已检测到你的安全短语和安全消息密钥被移除。", + "A new Security Phrase and key for Secure Messages have been detected.": "检测到新的安全短语和安全信息密钥。", + "Make a copy of your Security Key": "复制你的安全密钥", + "Your Security Key is in your Downloads folder.": "你的安全密钥在你的下载文件夹中。", + "Your Security Key has been copied to your clipboard, paste it to:": "你的安全密钥已 复制到你的粘贴板,将其粘贴至:", + "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "你的安全密钥是一张安全网——如果你忘记了你的安全短语的话,你可以用它来恢复你对已加密消息的访问权。", + "Repeat your Security Phrase...": "重复你的安全短语……", + "Enter your Security Phrase a second time to confirm it.": "再次输入你的安全短语进行确认。", + "Set up with a Security Key": "设置安全密钥", + "Great! This Security Phrase looks strong enough.": "棒!这个安全短语看着够强。", + "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "我们会将你的密钥的加密副本保存在我们的服务器上。使用安全短语来保证你的备份。", + "Space Autocomplete": "空间自动完成", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "未经验证,你将无法获取你的所有信息,且可能不被他人所信任。", + "Verify your identity to access encrypted messages and prove your identity to others.": "验证你的身份来获取已加密的消息并向其他人证明你的身份。", + "Use another login": "使用其他账号登录", + "Decide where your account is hosted": "决定账号托管位置", + "Host account on": "账号托管于", + "Already have an account? Sign in here": "已有账号?在此登录", + "That username already exists, please try another.": "用户名已存在,请试试别的。", + "New? Create account": "新来的?创建账号", + "Please choose a strong password": "请选择强密码", + "New here? Create an account": "新来的?创建账号", + "Got an account? Sign in": "有账号了?登录", + "Failed to find the general chat for this community": "找不到此社群的一般性聊天记录", + "We'll create rooms for each of them. You can add more later too, including already existing ones.": "我们将会为每个主题创建一个聊天室。你也可以稍后再进行添加,包括现有聊天室。", + "What projects are you working on?": "你正在从事哪些项目?", + "You can add more later too, including already existing ones.": "稍后你可以添加更多聊天室,包括现有的。", + "Let's create a room for each of them.": "让我们未每个主题都创建一个聊天室吧。", + "What are some things you want to discuss in %(spaceName)s?": "你想在 %(spaceName)s 中讨论什么?", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "这是实验性功能。当前收到邀请的新用户必须在上开启邀请才能真正加入。", + "Make sure the right people have access. You can invite more later.": "确保对的人可以访问。稍后你可以邀请更多人。", + "Invite your teammates": "邀请你的伙伴", + "Failed to invite the following users to your space: %(csvUsers)s": "邀请以下用户加入你的空间失败:%(csvUsers)s", + "A private space for you and your teammates": "供你和你的伙伴使用的私有空间", + "Me and my teammates": "我和我的伙伴", + "A private space to organise your rooms": "用于整理你聊天室的私有空间", + "Just me": "仅有我", + "Make sure the right people have access to %(name)s": "确保对的人有权访问 %(name)s", + "Who are you working with?": "你与谁一同工作?", + "Go to my space": "前往我的空间", + "Go to my first room": "前往我的第一个聊天室", + "It's just you at the moment, it will be even better with others.": "当前仅有你一人,与人同道而行会更好。", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "选择要添加的聊天室或对话。这是专属于你的空间,不会有人被通知。你稍后可以再增加更多。", + "Select a room below first": "首先选择一个聊天室", + "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s 个聊天室和 %(numSpaces)s 个空间", + "This room is suggested as a good one to join": "此聊天室很适合加入", + "You can select all or individual messages to retry or delete": "你可以选择全部或单独的消息来重试或删除", + "Sending": "正在发送", + "Delete all": "删除全部", + "Some of your messages have not been sent": "你的部分消息未被发送", + "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "你的消息未被发送,因为此主服务器已被其管理员封禁。请联络你的服务管理员已继续使用服务。", + "Filter all spaces": "过滤所有空间", + "You have no visible notifications.": "你没有可见的通知。", + "Communities are changing to Spaces": "社群正在转变为空间", + "%(creator)s created this DM.": "%(creator)s 创建了此私聊。", + "Verification requested": "已请求验证", + "Security Key mismatch": "安全密钥不符", + "Unable to set up keys": "无法设置密钥", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "如果你全部重置,你将会在没有受信任的会话重新开始、没有受信任的用户,且可能会看不到过去的消息。", + "Only do this if you have no other device to complete verification with.": "当你没有其他设备可以用于完成验证时,方可执行此操作。", + "Reset everything": "全部重置", + "Forgotten or lost all recovery methods? Reset all": "忘记或丢失了所有恢复方式?全部重置", + "Remember this": "记住", + "The widget will verify your user ID, but won't be able to perform actions for you:": "挂件将会验证你的用户 ID,但将无法为你执行动作:", + "Verify other login": "验证其他登录", + "Make this space private": "将此空间设为私有", + "Edit settings relating to your space.": "编辑关于你的空间的设置。", + "Reset event store": "重置活动存储", + "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": "如果这样做,请注意你的信息并不会被删除,但在重新建立索引是,搜索体验可能会退步一些", + "You most likely do not want to reset your event index store": "你大概率不想重置你的活动缩影存储", + "Reset event store?": "重置活动存储?", + "Use your preferred Matrix homeserver if you have one, or host your own.": "如果你可以使用自己所偏好的 Matrix 主服务器,或是自己搭建一个。", + "We call the places where you can host your account ‘homeservers’.": "我们将你可以托管账号的地方称为「主服务器」。", + "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org 是世界上最大的公开主服务器,因此对于许多人而言是个好地方。", + "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "这通常仅影响服务器如何处理聊天室。如果你的 %(brand)s 遇到问题,请回报错误。", + "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "请注意,如果你不添加电子邮箱并且忘记密码,你将永远失去对你账号的访问权。", + "Continuing without email": "不使用电子邮箱并继续", + "We recommend you change your password and Security Key in Settings immediately": "我们建议你立即在设置中更改你的密码和安全密钥", + "Data on this screen is shared with %(widgetDomain)s": "在此画面上的资料会与 %(widgetDomain)s 分享", + "Consult first": "先询问", + "Invited people will be able to read old messages.": "被邀请的人将能够阅读过去的消息。", + "Invite someone using their name, username (like ) or share this room.": "使用某人的名字、用户名(如 )或分享此聊天室来邀请他们。", + "Invite someone using their name, email address, username (like ) or share this room.": "使用某人的名字、电子邮箱地址或用户名来与他们开始对话(如 )或分享此聊天室。", + "Invite someone using their name, username (like ) or share this space.": "使用某人的名字、用户名(如 )邀请他们,或分享此空间。", + "Invite someone using their name, email address, username (like ) or share this space.": "使用某人的名字、电子邮箱地址或用户名(如 )邀请他们,或分享此空间。", + "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "这不会邀请他们加入 %(communityName)s。要邀请某人加入 %(communityName)s,请点击这里", + "Start a conversation with someone using their name or username (like ).": "使用某人的名字或用户名开始与其进行对话(如 )。", + "Start a conversation with someone using their name, email address or username (like ).": "使用某人的名称、电子邮箱地址或用户名来与其开始对话(如 )。", + "May include members not in %(communityName)s": "可能不包括 %(communityName)s 中的成员", + "A call can only be transferred to a single user.": "通话只能转移到单个用户。", + "We couldn't create your DM.": "我们无法创建你的私聊。", + "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "继续暂时允许让 %(hostSignupBrand)s 安装过程可以访问你的账号以获取已验证电子邮箱地址。此数据不保存。", + "Block anyone not part of %(serverName)s from ever joining this room.": "阻住任何不属于 %(serverName)s 的人加入此聊天室。", + "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "如果聊天室用于与自己的主服务器外的团队进行协助的话,可以停用此功能。这将无法在稍后进行更改。", + "What do you want to organise?": "你想要组织什么?", + "Skip for now": "暂时跳过", + "Failed to create initial space rooms": "创建初始空间聊天室失败", + "To join %(spaceName)s, turn on the Spaces beta": "加入 %(spaceName)s 前,请开启空间测试版", + "To view %(spaceName)s, turn on the Spaces beta": "查看 %(spaceName)s 前,请开启空间测试版", + "Private space": "私有空间", + "Public space": "公开空间", + "Spaces are a beta feature.": "空间为测试版功能。", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "如果你找不到正在寻找的聊天室,请请求邀请或创建一个新的聊天室。", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "私人聊天室仅能通过邀请找到与加入。公开聊天室则能够被所有人找到并加入。", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "私人聊天室仅能通过邀请找到与加入。公开聊天室则能够被所有在此社群的人找到并加入。", + "Search names and descriptions": "搜索名称和描述", + "You may want to try a different search or check for typos.": "你可能要尝试其他搜索或检查是否有错别字。", + "You may contact me if you have any follow up questions": "如果你有任何后续问题,可以联系我", + "To leave the beta, visit your settings.": "要退出测试版,请访问你的设置。", + "Your platform and username will be noted to help us use your feedback as much as we can.": "我们将会记录你的平台及用户名,以帮助我们尽我们所能地使用你的反馈。", + "%(featureName)s beta feedback": "%(featureName)s 测试版反馈", + "Thank you for your feedback, we really appreciate it.": "感谢你的反馈,我们衷心地感谢。", + "Beta feedback": "测试版反馈", + "Want to add a new room instead?": "想要添加一个新的聊天室吗?", + "Add existing rooms": "添加现有聊天室", + "You can add existing spaces to a space.": "你可以添加现有空间到另一空间中。", + "Feeling experimental?": "想要来点实验吗?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "正在新增聊天室……", + "Adding rooms... (%(progress)s out of %(count)s)|other": "正在新增聊天室……(%(count)s 中的第 %(progress)s 个)", + "Not all selected were added": "并非所有选中的都被添加", + "You are not allowed to view this server's rooms list": "你不被允许查看此服务器的聊天室列表", + "Sends the given message with a space themed effect": "此消息带有空间主题化效果", + "Kick, ban, or invite people to your active room, and make you leave": "移除、封禁或邀请人们到你所活跃的聊天室,并让你离开", + "Sends the given message as a spoiler": "此消息包含剧透", + "Retry all": "全部重试", + "View message": "查看消息", + "Zoom in": "放大", + "Zoom out": "缩小", + "%(count)s people you know have already joined|one": "已有你所认识的 %(count)s 个人加入", + "%(count)s people you know have already joined|other": "已有你所认识的 %(count)s 个人加入", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s 位成员中包括 %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "包括 %(commaSeparatedMembers)s", + "View all %(count)s members|one": "查看 1 位成员", + "View all %(count)s members|other": "查看全部 %(count)s 位成员", + "Use the Desktop app to search encrypted messages": "使用桌面端英语来搜索加密消息", + "Use the Desktop app to see all encrypted files": "使用桌面端应用来查看所有加密文件", + "Add reaction": "添加回应", + "Error processing voice message": "处理语音消息时发生错误", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "当你将自己降级后,你将无法撤销此更改。如果你是此空间的最后一名拥有权限的用户,则无法重新获得权限。", + "Unpin a widget to view it in this panel": "取消固定挂件以在此面板中查看", + "You can only pin up to %(count)s widgets|other": "你仅能固定 %(count)s 个挂件", + "Accept on your other login…": "接受你的其他登录……", + "Delete recording": "删除录制", + "Stop the recording": "停止录制", + "Record a voice message": "录制语音消息", + "We didn't find a microphone on your device. Please check your settings and try again.": "我们没能在你的设备上找到麦克风。请检查设置并重试。", + "No microphone found": "无法发现麦克风", + "We were unable to access your microphone. Please check your browser settings and try again.": "我们无法访问你的麦克风。 请检查浏览器设置并重试。", + "Unable to access your microphone": "无法访问你的麦克风", + "%(count)s results in all spaces|one": "在所有空间中有 %(count)s 个结果", + "%(count)s results in all spaces|other": "在所有空间中有 %(count)s 个结果", + "Quick actions": "快捷操作", + "You do not have permissions to add rooms to this space": "你没有权限添加聊天室至此空间", + "You do not have permissions to create new rooms in this space": "你没有权限在此空间内创建新的聊天室", + "Invite to just this room": "仅邀请至此聊天室", + "This is the beginning of your direct message history with .": "这是你与 间进行私聊的历史记录的开始。", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "除非你们其中一个邀请了别人加入,否则将仅有你们两个人在此对话中。", + "%(seconds)ss left": "剩余 %(seconds)s 秒", + "Failed to send": "发送失败", + "Change server ACLs": "更改服务器访问控制列表", + "You have no ignored users.": "你没有设置忽略用户。", + "Warn before quitting": "退出前警告", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "想要来点实验?实验室是提前体验、测试新功能并在它们正式发布前帮助它们定型的最佳方式。了解更多。", + "Your access token gives full access to your account. Do not share it with anyone.": "你的访问令牌可以完全访问你的帐户。不要将其与任何人分享。", + "Access Token": "访问令牌", + "Message search initialisation failed": "消息搜索初始化失败", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "使用 %(size)s 来存储来自 %(rooms)s 聊天室的消息。在本地安全地缓存已加密的消息以使其出现在搜索结果中。", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "使用 %(size)s 来存储来自 %(rooms)s 聊天室的消息。在本地安全地缓存已加密的消息以使其出现在搜索结果中。", + "Manage & explore rooms": "管理并探索聊天室", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "空间是一种将聊天室和人们进行分组的新方法。你需要得到邀请方可加入现有空间。", + "Please enter a name for the space": "请输入空间名称", + "Play": "播放", + "Pause": "暂停", + "%(name)s on hold": "保留 %(name)s", + "Connecting": "连接中", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "与 %(transferTarget)s 进行协商。转让至 %(transferee)s", + "unknown person": "陌生人", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "允许在一对一通话中使用点对点通讯(如果你启用此功能,对方可能会看到你的 IP 地址)", + "Send and receive voice messages": "发送并接收语音消息", + "Show options to enable 'Do not disturb' mode": "显示启用「请勿打扰」模式的选项", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "你的反馈将帮助空间变得更好。你能讲得越仔细越好。", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "测试版适用于网页端、桌面端以及安卓端。但在你的主服务器上有些特性可能不可用。", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "你随时可以在设置中退出测试版,或轻点如上所示的测试版徽章。", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s 将在空间启用时重载。社群和自定义标签将被隐藏。", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "测试版适用于网页端、桌面端以及安卓端。感谢你试用测试版。", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "如果你离开,%(brand)s 将会在空间禁用时重载。社群和自定义标记将再次可见。", + "Spaces are a new way to group rooms and people.": "空间是一种将聊天室与人们进行分组的新方式。", + "%(deviceId)s from %(ip)s": "来自 %(ip)s 的 %(deviceId)s", + "Review to ensure your account is safe": "检查以确保你的账号是安全的", + "See %(msgtype)s messages posted to your active room": "查看发布到你所活跃的聊天室的 %(msgtype)s 消息", + "See %(msgtype)s messages posted to this room": "查看发布到此聊天室的 %(msgtype)s 消息", + "Send %(msgtype)s messages as you in your active room": "在你所活跃的聊天室以你的身份发送 %(msgtype)s 消息", + "Send %(msgtype)s messages as you in this room": "在此聊天室以你的身份发送 %(msgtype)s 消息", + "See general files posted to your active room": "查看发布到你所活跃的聊天室的一般性文件", + "See general files posted to this room": "查看发布到此聊天室的一般性文件", + "Send general files as you in your active room": "在你所活跃的聊天室以你的身份发送一般性文件", + "Are you sure you want to leave the space '%(spaceName)s'?": "你确定要离开空间「%(spaceName)s」吗?", + "This space is not public. You will not be able to rejoin without an invite.": "此空间并不公开。在没有得到邀请的情况下,你将无法重新加入。", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "你是这里唯一的人。如果你离开了,以后包括你以在内任何人都将无法加入。", + "You do not have permission to create rooms in this community.": "你没有在此社群中建立聊天室的权限。", + "Now, let's help you get started": "现在,让我们协助你开始", + "Add a photo so people know it's you.": "添加照片,让人们知道这是你。", + "Great, that'll help people know it's you": "很好,这样大家就知道是你了", + "

    HTML for your community's page

    \n

    \n Use the long description to introduce new members to the community, or distribute\n some important links\n

    \n

    \n You can even add images with Matrix URLs \n

    \n": "

    社群页面的 HTML

    \n

    \n 使用详细秒速向社群介绍新成员,或分发\n 一些重要链接\n

    \n

    \n 你甚至可以使用 Matrix 链接 新增图片\n

    \n", + "Use email to optionally be discoverable by existing contacts.": "使用电子邮箱以选择性地被现有联系人搜索。", + "Use email or phone to optionally be discoverable by existing contacts.": "使用电子邮箱或电话以选择性地被现有联系人搜索。", + "Add an email to be able to reset your password.": "添加电子邮箱以重置你的密码。", + "That phone number doesn't look quite right, please check and try again": "电话号码看起来不太对,请检查并重试", + "Something went wrong in confirming your identity. Cancel and try again.": "确认你的身份时出了一点问题。取消并重试。", + "Open the link in the email to continue registration.": "打开电子邮件中的链接以继续注册。", + "A confirmation email has been sent to %(emailAddress)s": "确认电子邮件以发送至 %(emailAddress)s", + "Avatar": "头像", + "Join the beta": "加入测试版", + "Leave the beta": "退出测试版", + "Beta": "测试版", + "Tap for more info": "点击以获取更多信息", + "Spaces is a beta feature": "空间为测试功能", + "Start audio stream": "开始音频流", + "Failed to start livestream": "开始流直播失败", + "Unable to start audio streaming.": "无法开始音频流媒体。", + "Hold": "挂起", + "Resume": "恢复", + "If you've forgotten your Security Key you can ": "如果你忘记了你的安全密钥,你可以", + "Access your secure message history and set up secure messaging by entering your Security Key.": "通过输入你的安全密钥来访问你的安全消息历史记录并设置安全通信。", + "Not a valid Security Key": "安全密钥无效", + "This looks like a valid Security Key!": "看起来是有效的安全密钥!", + "If you've forgotten your Security Phrase you can use your Security Key or set up new recovery options": "如果你忘记了你的安全短语,你可以使用你的安全密钥设置新的恢复选项", + "Access your secure message history and set up secure messaging by entering your Security Phrase.": "无法通过你的安全短语访问你的安全消息历史记录并设置安全通信。", + "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "无法使用此安全短语解密备份:请确认你是否输入了正确的安全短语。", + "Incorrect Security Phrase": "安全短语错误", + "Send general files as you in this room": "查看发布到此聊天室的一般性文件", + "See videos posted to your active room": "查看发布到你所活跃的聊天室的视频", + "See videos posted to this room": "查看发布到此聊天室的视频", + "Send videos as you in your active room": "查看发布到你所活跃的聊天室的视频", + "Send videos as you in this room": "查看发布到此聊天室的视频", + "See images posted to your active room": "查看发布到你所活跃的聊天室的图片", + "See images posted to this room": "查看发布到此聊天室的图片", + "Send images as you in your active room": "在你所活跃的聊天室以你的身份发送图片", + "Send images as you in this room": "在此聊天室以你的身份发送图片", + "See emotes posted to your active room": "查看发布到你所活跃的聊天室的表情", + "See emotes posted to this room": "查看发布到此聊天室的表情", + "Send emotes as you in your active room": "在你所活跃的聊天室以你的身份发送表情", + "Send emotes as you in this room": "在此聊天室以你的身份发送表情", + "See text messages posted to your active room": "查看发布到你所活跃的聊天室的文本消息", + "See text messages posted to this room": "查看发布到此聊天室的文本消息", + "Send text messages as you in your active room": "在你所活跃的聊天室以你的身份发送文本消息", + "Send text messages as you in this room": "在此聊天室以你的身份发送文本消息", + "See messages posted to your active room": "查看发布到你所活跃的聊天室的消息", + "See messages posted to this room": "查看发布到此聊天室的消息", + "Send messages as you in your active room": "在你所活跃的聊天室以你的身份发送消息", + "Send messages as you in this room": "在此聊天室以你的身份发送消息", + "See when anyone posts a sticker to your active room": "查看何时有人发送贴纸到你所活跃的聊天室", + "Send stickers to your active room as you": "发送贴纸到你所活跃的聊天室", + "See when people join, leave, or are invited to your active room": "查看人们何时加入、离开或被邀请到你所活跃的聊天室", + "See when people join, leave, or are invited to this room": "查看人们何时加入、离开或被邀请到这个房间", + "Kick, ban, or invite people to this room, and make you leave": "移除、封禁或邀请用户到此聊天室,并让你离开" } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 134f61a435..5c27fb3878 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -169,9 +169,9 @@ "No Webcams detected": "未偵測到網路攝影機", "No media permissions": "沒有媒體權限", "You may need to manually permit %(brand)s to access your microphone/webcam": "您可能需要手動允許 %(brand)s 存取您的麥克風/網路攝影機", - "Are you sure you want to leave the room '%(roomName)s'?": "您確定您要想要離開房間 '%(roomName)s' 嗎?", + "Are you sure you want to leave the room '%(roomName)s'?": "你確定你要想要離開房間 '%(roomName)s' 嗎?", "Bans user with given id": "阻擋指定 ID 的使用者", - "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "無法連線到家伺服器 - 請檢查您的連線,確保您的家伺服器的 SSL 憑證可被信任,而瀏覽器擴充套件也沒有阻擋請求。", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "無法連線到家伺服器 - 請檢查你的連線,確保你的家伺服器的 SSL 憑證可被信任,而瀏覽器擴充套件也沒有阻擋請求。", "%(senderName)s changed their profile picture.": "%(senderName)s 已經變更了他的基本資料圖片。", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s 變更了 %(powerLevelDiffText)s 權限等級。", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s 將聊天室名稱變更為 %(roomName)s。", @@ -838,7 +838,7 @@ "An error ocurred whilst trying to remove the widget from the room": "嘗試從聊天室移除小工具時發生錯誤", "System Alerts": "系統警告", "Only room administrators will see this warning": "僅聊天室管理員會看到此警告", - "Please contact your service administrator to continue using the service.": "請聯絡您的服務管理員以繼續使用服務。", + "Please contact your service administrator to continue using the service.": "請聯絡你的服務管理員以繼續使用服務。", "This homeserver has hit its Monthly Active User limit.": "這個主伺服器已經到達其每月活躍使用者限制。", "This homeserver has exceeded one of its resource limits.": "此主伺服器已經超過其中一項資源限制。", "Upgrade Room Version": "更新聊天室版本", @@ -1160,7 +1160,7 @@ "Change": "變更", "Couldn't load page": "無法載入頁面", "This homeserver does not support communities": "此家伺服器不支援社群", - "A verification email will be sent to your inbox to confirm setting your new password.": "一封驗證用的電子郵件已經傳送到您的收件匣以確認您設定了新密碼。", + "A verification email will be sent to your inbox to confirm setting your new password.": "一封驗證用的電子郵件已經傳送到你的收件匣以確認你設定了新密碼。", "Your password has been reset.": "您的密碼已重設。", "This homeserver does not support login using email address.": "此家伺服器不支援使用電子郵件地址登入。", "Registration has been disabled on this homeserver.": "註冊已在此家伺服器上停用。", @@ -1973,8 +1973,8 @@ "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s 為此聊天室新增了替代位置 %(addresses)s。", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s 為此聊天室移除了替代位置 %(addresses)s。", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s 為此聊天室移除了替代位置 %(addresses)s。", - "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s 為此聊天是變更了替代位置。", - "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s 為此聊天是變更了主要及替代位置。", + "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s 為此聊天室變更了替代位置。", + "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s 為此聊天室變更了主要及替代位置。", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "更新聊天室的替代位置時發生錯誤。伺服器可能不允許這麼做,或是昱到了暫時性的故障。", "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s 將聊天室名稱從 %(oldRoomName)s 變更為 %(newRoomName)s。", "%(senderName)s changed the addresses for this room.": "%(senderName)s 變更了此聊天室的位置。", @@ -2889,7 +2889,7 @@ "Send messages as you in this room": "在此聊天室以您的身份傳送訊息", "The %(capability)s capability": "%(capability)s 能力", "See %(eventType)s events posted to your active room": "檢視發佈到您的活躍聊天室的 %(eventType)s 活動", - "Send %(eventType)s events as you in your active room": "以您的身份在您的活躍聊天是傳送 %(eventType)s 活動", + "Send %(eventType)s events as you in your active room": "以您的身份在您的活躍聊天室傳送 %(eventType)s 活動", "See %(eventType)s events posted to this room": "檢視發佈到此聊天室的 %(eventType)s 活動", "Send %(eventType)s events as you in this room": "以您的身份在此聊天室傳送 %(eventType)s 活動", "with state key %(stateKey)s": "帶有狀態金鑰 %(stateKey)s", @@ -2898,17 +2898,17 @@ "Send stickers to your active room as you": "以您的身份傳送貼圖到您活躍的聊天室", "See when a sticker is posted in this room": "檢視貼圖在此聊天室中何時貼出", "Send stickers to this room as you": "以您的身份傳送貼圖到此聊天室", - "See when the avatar changes in your active room": "檢視您活躍聊天是的大頭照何時變更", - "Change the avatar of your active room": "變更您活躍聊天是的大頭照", - "See when the avatar changes in this room": "檢視此聊天是的大頭照何時變更", + "See when the avatar changes in your active room": "檢視您活躍聊天室的大頭照何時變更", + "Change the avatar of your active room": "變更您活躍聊天室的大頭照", + "See when the avatar changes in this room": "檢視此聊天室的大頭照何時變更", "Change the avatar of this room": "變更此聊天室的大頭照", "See when the name changes in your active room": "檢視您活躍聊天室的名稱何時變更", "Change the name of your active room": "變更您活躍聊天室的名稱", - "See when the name changes in this room": "檢視此聊天是的名稱何時變更", + "See when the name changes in this room": "檢視此聊天室的名稱何時變更", "Change the name of this room": "變更此聊天室的名稱", - "See when the topic changes in your active room": "檢視您活躍的聊天是的主題何時變更", - "Change the topic of your active room": "變更您活躍聊天是的主題", - "See when the topic changes in this room": "檢視此聊天是的主題何時變更", + "See when the topic changes in your active room": "檢視您活躍的聊天室的主題何時變更", + "Change the topic of your active room": "變更您活躍聊天室的主題", + "See when the topic changes in this room": "檢視此聊天室的主題何時變更", "Change the topic of this room": "變更此聊天室的主題", "Change which room you're viewing": "變更您正在檢視的聊天室", "Send stickers into your active room": "傳送貼圖到您活躍的聊天室", @@ -3226,7 +3226,7 @@ "Mark as suggested": "標記為建議", "Mark as not suggested": "標記為不建議", "Removing...": "正在移除……", - "Failed to remove some rooms. Try again later": "移除部份聊天是失敗。稍後再試", + "Failed to remove some rooms. Try again later": "移除部份聊天室失敗。稍後再試", "%(count)s rooms and 1 space|one": "%(count)s 個聊天室與 1 個空間", "%(count)s rooms and 1 space|other": "%(count)s 個聊天室與 1 個空間", "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s 個聊天室與 %(numSpaces)s 個空間", @@ -3240,7 +3240,7 @@ "Open": "開啟", "%(count)s messages deleted.|one": "已刪除 %(count)s 則訊息。", "%(count)s messages deleted.|other": "已刪除 %(count)s 則訊息。", - "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "這通常只影響伺服器如何處理聊天是。如果您的 %(brand)s 遇到問題,請回報臭蟲。", + "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "這通常只影響伺服器如何處理聊天室。如果您的 %(brand)s 遇到問題,請回報臭蟲。", "Invite to %(roomName)s": "邀請至 %(roomName)s", "Edit devices": "編輯裝置", "Invite People": "邀請夥伴", @@ -3311,5 +3311,72 @@ "Including %(commaSeparatedMembers)s": "包含 %(commaSeparatedMembers)s", "View all %(count)s members|one": "檢視 1 個成員", "View all %(count)s members|other": "檢視全部 %(count)s 個成員", - "Failed to send": "傳送失敗" + "Failed to send": "傳送失敗", + "Enter your Security Phrase a second time to confirm it.": "再次輸入您的安全密語以進行確認。", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "挑選要新增的聊天室或對話。這是專屬於您的空間,不會有人被通知。您稍後可以再新增更多。", + "What do you want to organise?": "您想要整理什麼?", + "Filter all spaces": "過濾所有空間", + "Delete recording": "刪除錄製", + "Stop the recording": "停止錄製", + "%(count)s results in all spaces|one": "所有空間中有 %(count)s 個結果", + "%(count)s results in all spaces|other": "所有空間中有 %(count)s 個結果", + "You have no ignored users.": "您沒有忽略的使用者。", + "Play": "播放", + "Pause": "暫停", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "這是實驗性功能。目前,收到邀請的新使用者必須在 上開啟邀請才能真的加入。", + "To join %(spaceName)s, turn on the Spaces beta": "要加入 %(spaceName)s,請開啟空間測試版", + "To view %(spaceName)s, turn on the Spaces beta": "要檢視 %(spaceName)s,開啟空間測試版", + "Select a room below first": "首先選取一個聊天室", + "Communities are changing to Spaces": "社群正在變更為空間", + "Join the beta": "加入測試版", + "Leave the beta": "離開測試版", + "Beta": "測試", + "Tap for more info": "點擊以取得更多資訊", + "Spaces is a beta feature": "空間為測試功能", + "Want to add a new room instead?": "想要新增新聊天室嗎?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "正在新增聊天室……", + "Adding rooms... (%(progress)s out of %(count)s)|other": "正在新增聊天室……(%(count)s 中的第 %(progress)s 個)", + "Not all selected were added": "並非所有選定的都被新增了", + "You can add existing spaces to a space.": "您可以新增既有的空間至空間中。", + "Feeling experimental?": "想要來點實驗嗎?", + "You are not allowed to view this server's rooms list": "您不被允許檢視此伺服器的聊天室清單", + "Error processing voice message": "處理語音訊息時發生錯誤", + "We didn't find a microphone on your device. Please check your settings and try again.": "我們在您的裝置上找不到麥克風。請檢查您的設定並再試一次。", + "No microphone found": "找不到麥克風", + "We were unable to access your microphone. Please check your browser settings and try again.": "我們無法存取您的麥克風。請檢查您的瀏覽器設定並再試一次。", + "Unable to access your microphone": "無法存取您的麥克風", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "想要來點實驗嗎?實驗室是儘早取得成果,測試新功能並在實際發佈前協助塑造它們的最佳方式。取得更多資訊。", + "Your access token gives full access to your account. Do not share it with anyone.": "您的存取權杖可給您帳號完整的存取權限。不要將其與任何人分享。", + "Access Token": "存取權杖", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "空間是將聊天室與人們分組的一種新方式。要加入既有的空間,您需要邀請。", + "Please enter a name for the space": "請輸入空間名稱", + "Connecting": "正在連線", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "允許在 1:1 通話中使用點對點通訊(若您啟用此功能,對方就能看到您的 IP 位置)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "供網頁、桌面與 Android 使用的測試版。部份功能可能在您的家伺服器上不可用。", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "您可以隨時從設定中退出測試版,或是點擊測試版徽章,例如上面那個。", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s 將在啟用空間的情況下重新載入。社群與自訂標籤將會隱藏。", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "測試版可用於網路、桌面與 Android。感謝您試用測試版。", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s 將在停用空間的情況下重新載入。社群與自訂標籤將再次可見。", + "Spaces are a new way to group rooms and people.": "空間是將聊天室與人們分組的一種新方式。", + "Message search initialisation failed": "訊息搜尋初始化失敗", + "Spaces are a beta feature.": "空間為測試版功能。", + "Search names and descriptions": "搜尋名稱與描述", + "You may contact me if you have any follow up questions": "如果您還有任何後續問題,可以聯絡我", + "To leave the beta, visit your settings.": "要離開測試版,請造訪您的設定。", + "Your platform and username will be noted to help us use your feedback as much as we can.": "我們將會記錄您的平台與使用者名稱,以協助我們盡可能使用您的回饋。", + "%(featureName)s beta feedback": "%(featureName)s 測試版回饋", + "Thank you for your feedback, we really appreciate it.": "感謝您的回饋,我們衷心感謝。", + "Beta feedback": "測試版回饋", + "Add reaction": "新增反應", + "Send and receive voice messages": "傳送與接收語音訊息", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "您的回饋意見將會讓空間變得更好。您可以輸入愈多細節愈好。", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "若您離開,%(brand)s 將在停用空間的情況下重新載入。社群與自訂標籤將再次可見。", + "Space Autocomplete": "空間自動完成", + "Go to my space": "到我的空間", + "sends space invaders": "傳送太空侵略者", + "Sends the given message with a space themed effect": "與太空主題效果一起傳送指定的訊息", + "See when people join, leave, or are invited to your active room": "檢視人們何時加入、離開或被邀請至您活躍的聊天室", + "Kick, ban, or invite people to your active room, and make you leave": "踢除、封鎖或邀請人們到您作用中的聊天室,然後讓您離開", + "See when people join, leave, or are invited to this room": "檢視人們何時加入、離開或被邀請至此聊天室", + "Kick, ban, or invite people to this room, and make you leave": "踢除、封鎖或邀請人們到此聊天室,然後讓您離開" } diff --git a/src/identifiers.ts b/src/identifiers.ts new file mode 100644 index 0000000000..cc8b2fee4d --- /dev/null +++ b/src/identifiers.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const ELEMENT_CLIENT_ID = "io.element.web"; diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 1cb44f240d..33f2d594ae 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -38,7 +38,6 @@ export default class EventIndex extends EventEmitter { this._eventsPerCrawl = 100; this._crawler = null; this._currentCheckpoint = null; - this.liveEventsForIndex = new Set(); } async init() { @@ -178,8 +177,10 @@ export default class EventIndex extends EventEmitter { * listener. */ onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => { + const client = MatrixClientPeg.get(); + // We only index encrypted rooms locally. - if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; + if (!client.isRoomEncrypted(room.roomId)) return; // If it isn't a live event or if it's redacted there's nothing to // do. @@ -188,16 +189,9 @@ export default class EventIndex extends EventEmitter { return; } - // If the event is not yet decrypted mark it for the - // Event.decrypted callback. - if (ev.isBeingDecrypted()) { - const eventId = ev.getId(); - this.liveEventsForIndex.add(eventId); - } else { - // If the event is decrypted or is unencrypted add it to the - // index now. - await this.addLiveEventToIndex(ev); - } + await client.decryptEventIfNeeded(ev); + + await this.addLiveEventToIndex(ev); } onRoomStateEvent = async (ev, state) => { @@ -216,10 +210,7 @@ export default class EventIndex extends EventEmitter { * listener, if so queues it up to be added to the index. */ onEventDecrypted = async (ev, err) => { - const eventId = ev.getId(); - // If the event isn't in our live event set, ignore it. - if (!this.liveEventsForIndex.delete(eventId)) return; if (err) return; await this.addLiveEventToIndex(ev); } @@ -462,7 +453,7 @@ export default class EventIndex extends EventEmitter { let res; try { - res = await client._createMessagesRequest( + res = await client.createMessagesRequest( checkpoint.roomId, checkpoint.token, this._eventsPerCrawl, checkpoint.direction); } catch (e) { @@ -523,18 +514,14 @@ export default class EventIndex extends EventEmitter { } }); - const decryptionPromises = []; - - matrixEvents.forEach(ev => { - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { - // TODO the decryption promise is a private property, this - // should either be made public or we should convert the - // event that gets fired when decryption is done into a - // promise using the once event emitter method: - // https://nodejs.org/api/events.html#events_events_once_emitter_name - decryptionPromises.push(ev._decryptionPromise); - } - }); + const decryptionPromises = matrixEvents + .filter(event => event.isEncrypted()) + .map(event => { + return client.decryptEventIfNeeded(event, { + isRetry: true, + emit: false, + }); + }); // Let us wait for all the events to get decrypted. await Promise.all(decryptionPromises); diff --git a/src/integrations/IntegrationManagers.ts b/src/integrations/IntegrationManagers.ts index a29c74c5eb..780f6d4660 100644 --- a/src/integrations/IntegrationManagers.ts +++ b/src/integrations/IntegrationManagers.ts @@ -28,6 +28,7 @@ import WidgetUtils from "../utils/WidgetUtils"; import {MatrixClientPeg} from "../MatrixClientPeg"; import SettingsStore from "../settings/SettingsStore"; import url from 'url'; +import { compare } from "../utils/strings"; const KIND_PREFERENCE = [ // Ordered: first is most preferred, last is least preferred. @@ -152,7 +153,7 @@ export class IntegrationManagers { if (kind === Kind.Account) { // Order by state_keys (IDs) - managers.sort((a, b) => a.id.localeCompare(b.id)); + managers.sort((a, b) => compare(a.id, b.id)); } ordered.push(...managers); diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index b61f57d4b3..16950dc008 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -56,6 +56,15 @@ export function newTranslatableError(message: string) { return error; } +export function getUserLanguage(): string { + const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); + if (language) { + return language; + } else { + return normalizeLanguageKey(getLanguageFromBrowser()); + } +} + // Function which only purpose is to mark that a string is translatable // Does not actually do anything. It's helpful for automatic extraction of translatable strings export function _td(s: string): string { @@ -96,12 +105,14 @@ function safeCounterpartTranslate(text: string, options?: object) { return translated; } +type SubstitutionValue = number | string | React.ReactNode | ((sub: string) => React.ReactNode); + export interface IVariables { count?: number; - [key: string]: number | string; + [key: string]: SubstitutionValue; } -type Tags = Record React.ReactNode>; +type Tags = Record; export type TranslatedString = string | React.ReactNode; @@ -238,7 +249,7 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri let replaced; // If substitution is a function, call it if (mapping[regexpString] instanceof Function) { - replaced = (mapping as Tags)[regexpString].apply(null, capturedGroups); + replaced = ((mapping as Tags)[regexpString] as Function)(...capturedGroups); } else { replaced = mapping[regexpString]; } @@ -335,7 +346,10 @@ export function setLanguage(preferredLangs: string | string[]) { counterpart.registerTranslations(langToUse, langData); counterpart.setLocale(langToUse); SettingsStore.setValue("language", null, SettingLevel.DEVICE, langToUse); - console.log("set language to " + langToUse); + // Adds a lot of noise to test runs, so disable logging there. + if (process.env.NODE_ENV !== "test") { + console.log("set language to " + langToUse); + } // Set 'en' as fallback language: if (langToUse !== "en") { @@ -455,10 +469,14 @@ function getLangsJson(): Promise { request( { method: "GET", url }, (err, response, body) => { - if (err || response.status < 200 || response.status >= 300) { + if (err) { reject(err); return; } + if (response.status < 200 || response.status >= 300) { + reject(new Error(`Failed to load ${url}, got ${response.status}`)); + return; + } resolve(JSON.parse(body)); }, ); @@ -498,10 +516,14 @@ function getLanguage(langPath: string): Promise { request( { method: "GET", url: langPath }, (err, response, body) => { - if (err || response.status < 200 || response.status >= 300) { + if (err) { reject(err); return; } + if (response.status < 200 || response.status >= 300) { + reject(new Error(`Failed to load ${langPath}, got ${response.status}`)); + return; + } resolve(weblateToCounterpart(JSON.parse(body))); }, ); diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index 84a131f23a..feda257d8b 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -254,11 +254,15 @@ matrixLinkify.options = { target: function(href, type) { if (type === 'url') { - const transformed = tryTransformPermalinkToLocalHref(href); - if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) { - return null; - } else { - return '_blank'; + try { + const transformed = tryTransformPermalinkToLocalHref(href); + if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) { + return null; + } else { + return '_blank'; + } + } catch (e) { + // malformed URI } } return null; diff --git a/src/performance/entry-names.ts b/src/performance/entry-names.ts new file mode 100644 index 0000000000..effd9506f6 --- /dev/null +++ b/src/performance/entry-names.ts @@ -0,0 +1,57 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export enum PerformanceEntryNames { + + /** + * Application wide + */ + + APP_STARTUP = "mx_AppStartup", + PAGE_CHANGE = "mx_PageChange", + + /** + * Events + */ + + RESEND_EVENT = "mx_ResendEvent", + SEND_E2EE_EVENT = "mx_SendE2EEEvent", + SEND_ATTACHMENT = "mx_SendAttachment", + + /** + * Rooms + */ + + SWITCH_ROOM = "mx_SwithRoom", + JUMP_TO_ROOM = "mx_JumpToRoom", + JOIN_ROOM = "mx_JoinRoom", + CREATE_DM = "mx_CreateDM", + PEEK_ROOM = "mx_PeekRoom", + + /** + * User + */ + + VERIFY_E2EE_USER = "mx_VerifyE2EEUser", + LOGIN = "mx_Login", + REGISTER = "mx_Register", + + /** + * VoIP + */ + + SETUP_VOIP_CALL = "mx_SetupVoIPCall", +} diff --git a/src/performance/index.ts b/src/performance/index.ts new file mode 100644 index 0000000000..bfb5b4a9c7 --- /dev/null +++ b/src/performance/index.ts @@ -0,0 +1,178 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { PerformanceEntryNames } from "./entry-names"; + +interface GetEntriesOptions { + name?: string, + type?: string, +} + +type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void; + +interface PerformanceDataListener { + entryNames?: string[], + callback: PerformanceCallbackFunction +} + +export default class PerformanceMonitor { + static _instance: PerformanceMonitor; + + private START_PREFIX = "start:" + private STOP_PREFIX = "stop:" + + private listeners: PerformanceDataListener[] = [] + private entries: PerformanceEntry[] = [] + + public static get instance(): PerformanceMonitor { + if (!PerformanceMonitor._instance) { + PerformanceMonitor._instance = new PerformanceMonitor(); + } + return PerformanceMonitor._instance; + } + + /** + * Starts a performance recording + * @param name Name of the recording + * @param id Specify an identifier appended to the measurement name + * @returns {void} + */ + start(name: string, id?: string): void { + if (!this.supportsPerformanceApi()) { + return; + } + const key = this.buildKey(name, id); + + if (performance.getEntriesByName(this.START_PREFIX + key).length > 0) { + console.warn(`Recording already started for: ${name}`); + return; + } + + performance.mark(this.START_PREFIX + key); + } + + /** + * Stops a performance recording and stores delta duration + * with the start marker + * @param name Name of the recording + * @param id Specify an identifier appended to the measurement name + * @returns {void} + */ + stop(name: string, id?: string): PerformanceEntry { + if (!this.supportsPerformanceApi()) { + return; + } + const key = this.buildKey(name, id); + if (performance.getEntriesByName(this.START_PREFIX + key).length === 0) { + console.warn(`No recording started for: ${name}`); + return; + } + + performance.mark(this.STOP_PREFIX + key); + performance.measure( + key, + this.START_PREFIX + key, + this.STOP_PREFIX + key, + ); + + this.clear(name, id); + + const measurement = performance.getEntriesByName(key).pop(); + + // Keeping a reference to all PerformanceEntry created + // by this abstraction for historical events collection + // when adding a data callback + this.entries.push(measurement); + + this.listeners.forEach(listener => { + if (this.shouldEmit(listener, measurement)) { + listener.callback([measurement]) + } + }); + + return measurement; + } + + clear(name: string, id?: string): void { + if (!this.supportsPerformanceApi()) { + return; + } + const key = this.buildKey(name, id); + performance.clearMarks(this.START_PREFIX + key); + performance.clearMarks(this.STOP_PREFIX + key); + } + + getEntries({ name, type }: GetEntriesOptions = {}): PerformanceEntry[] { + return this.entries.filter(entry => { + const satisfiesName = !name || entry.name === name; + const satisfiedType = !type || entry.entryType === type; + return satisfiesName && satisfiedType; + }); + } + + addPerformanceDataCallback(listener: PerformanceDataListener, buffer = false) { + this.listeners.push(listener); + if (buffer) { + const toEmit = this.entries.filter(entry => this.shouldEmit(listener, entry)); + if (toEmit.length > 0) { + listener.callback(toEmit); + } + } + } + + removePerformanceDataCallback(callback?: PerformanceCallbackFunction) { + if (!callback) { + this.listeners = []; + } else { + this.listeners.splice( + this.listeners.findIndex(listener => listener.callback === callback), + 1, + ); + } + } + + /** + * Tor browser does not support the Performance API + * @returns {boolean} true if the Performance API is supported + */ + private supportsPerformanceApi(): boolean { + return performance !== undefined && performance.mark !== undefined; + } + + private shouldEmit(listener: PerformanceDataListener, entry: PerformanceEntry): boolean { + return !listener.entryNames || listener.entryNames.includes(entry.name); + } + + /** + * Internal utility to ensure consistent name for the recording + * @param name Name of the recording + * @param id Specify an identifier appended to the measurement name + * @returns {string} a compound of the name and identifier if present + */ + private buildKey(name: string, id?: string): string { + return `${name}${id ? `:${id}` : ''}`; + } +} + + +// Convenience exports +export { + PerformanceEntryNames, +} + +// Exposing those to the window object to bridge them from tests +window.mxPerformanceMonitor = PerformanceMonitor.instance; +window.mxPerformanceEntryNames = PerformanceEntryNames; diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index b886f369df..9512f62e42 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -73,7 +73,9 @@ class ConsoleLogger { // Convert objects and errors to helpful things args = args.map((arg) => { - if (arg instanceof Error) { + if (arg instanceof DOMException) { + return arg.message + ` (${arg.name} | ${arg.code}) ` + (arg.stack ? `\n${arg.stack}` : ''); + } else if (arg instanceof Error) { return arg.message + (arg.stack ? `\n${arg.stack}` : ''); } else if (typeof (arg) === 'object') { try { diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 29856b1a86..b2ad9fe6f6 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -28,6 +28,7 @@ import * as rageshake from './rageshake'; // polyfill textencoder if necessary import * as TextEncodingUtf8 from 'text-encoding-utf-8'; import SettingsStore from "../settings/SettingsStore"; +import SdkConfig from "../SdkConfig"; let TextEncoder = window.TextEncoder; if (!TextEncoder) { TextEncoder = TextEncodingUtf8.TextEncoder; @@ -91,8 +92,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append('cross_signing_key', client.getCrossSigningId()); // add cross-signing status information - const crossSigning = client._crypto._crossSigningInfo; - const secretStorage = client._crypto._secretStorage; + const crossSigning = client.crypto._crossSigningInfo; + const secretStorage = client.crypto._secretStorage; body.append("cross_signing_ready", String(await client.isCrossSigningReady())); body.append("cross_signing_supported_by_hs", @@ -113,7 +114,7 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append("secret_storage_key_in_account", String(!!(await secretStorage.hasKey()))); body.append("session_backup_key_in_secret_storage", String(!!(await client.isKeyBackupKeyStored()))); - const sessionBackupKeyFromCache = await client._crypto.getSessionBackupPrivateKey(); + const sessionBackupKeyFromCache = await client.crypto.getSessionBackupPrivateKey(); body.append("session_backup_key_cached", String(!!sessionBackupKeyFromCache)); body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array)); } @@ -268,6 +269,25 @@ function uint8ToString(buf: Buffer) { return out; } +export async function submitFeedback(endpoint: string, label: string, comment: string, canContact = false) { + let version = "UNKNOWN"; + try { + version = await PlatformPeg.get().getAppVersion(); + } catch (err) {} // PlatformPeg already logs this. + + const body = new FormData(); + body.append("label", label); + body.append("text", comment); + body.append("can_contact", canContact ? "yes" : "no"); + + body.append("app", "element-web"); + body.append("version", version); + body.append("platform", PlatformPeg.get().getHumanReadableName()); + body.append("user_id", MatrixClientPeg.get()?.getUserId()); + + await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {}); +} + function _submitReport(endpoint: string, body: FormData, progressCallback: (string) => void) { return new Promise((resolve, reject) => { const req = new XMLHttpRequest(); diff --git a/src/settings/Settings.ts b/src/settings/Settings.tsx similarity index 93% rename from src/settings/Settings.ts rename to src/settings/Settings.tsx index 1497a2208d..155d039572 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.tsx @@ -16,8 +16,9 @@ limitations under the License. */ import { MatrixClient } from 'matrix-js-sdk/src/client'; +import React, { ReactNode } from "react"; -import { _td } from '../languageHandler'; +import { _t, _td } from '../languageHandler'; import { NotificationBodyEnabledController, NotificationsEnabledController, @@ -39,6 +40,7 @@ import { OrderedMultiController } from "./controllers/OrderedMultiController"; import { Layout } from "./Layout"; import ReducedMotionController from './controllers/ReducedMotionController'; import IncompatibleController from "./controllers/IncompatibleController"; +import SdkConfig from "../SdkConfig"; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -117,6 +119,15 @@ export interface ISetting { // historical settings which we don't want existing user's values be wiped. Do // not use this for new settings. invertedSettingName?: string; + + betaInfo?: { + title: string; // _td + caption: string; // _td + disclaimer?: (enabled: boolean) => ReactNode; + image: string; // require(...) + feedbackSubheading?: string; + feedbackLabel?: string; + }; } export const SETTINGS: {[setting: string]: ISetting} = { @@ -127,6 +138,36 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, controller: new ReloadOnChangeController(), + betaInfo: { + title: _td("Spaces"), + caption: _td("Spaces are a new way to group rooms and people."), + disclaimer: (enabled) => { + if (enabled) { + return <> +

    { _t("If you leave, %(brand)s will reload with Spaces disabled. " + + "Communities and custom tags will be visible again.", { + brand: SdkConfig.get().brand, + }) }

    +

    { _t("Beta available for web, desktop and Android. Thank you for trying the beta.") }

    + ; + } + + return <> +

    { _t("%(brand)s will reload with Spaces enabled. " + + "Communities and custom tags will be hidden.", { + brand: SdkConfig.get().brand, + }) }

    + { _t("You can leave the beta any time from settings or tapping on a beta badge, " + + "like the one above.") } +

    { _t("Beta available for web, desktop and Android. " + + "Some features may be unavailable on your homeserver.") }

    + ; + }, + image: require("../../res/img/betas/spaces.png"), + feedbackSubheading: _td("Your feedback will help make spaces better. " + + "The more detail you can go into, the better."), + feedbackLabel: "spaces-feedback", + }, }, "feature_dnd": { isFeature: true, @@ -136,7 +177,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "feature_voice_messages": { isFeature: true, - displayName: _td("Send and receive voice messages (in development)"), + displayName: _td("Send and receive voice messages"), supportedLevels: LEVELS_FEATURE, default: false, }, @@ -156,12 +197,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: false, controller: new IncompatibleController("feature_spaces"), }, - "feature_new_spinner": { - isFeature: true, - displayName: _td("New spinner design"), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_pinning": { isFeature: true, displayName: _td("Message Pinning"), @@ -566,10 +601,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td('Enable widget screenshots on supported widgets'), default: false, }, - "PinnedEvents.isOpen": { - supportedLevels: [SettingLevel.ROOM_DEVICE], - default: false, - }, "promptBeforeInviteUnknownUsers": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Prompt before sending invites to potentially invalid matrix IDs'), @@ -689,7 +720,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: Layout.Group, }, "showChatEffects": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, + supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM, displayName: _td("Show chat effects (animations when receiving e.g. confetti)"), default: true, controller: new ReducedMotionController(), diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index c2675bd8f8..e1e300e185 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -26,7 +26,7 @@ import { _t } from '../languageHandler'; import dis from '../dispatcher/dispatcher'; import { ISetting, SETTINGS } from "./Settings"; import LocalEchoWrapper from "./handlers/LocalEchoWrapper"; -import { WatchManager } from "./WatchManager"; +import { WatchManager, CallbackFn as WatchCallbackFn } from "./WatchManager"; import { SettingLevel } from "./SettingLevel"; import SettingsHandler from "./handlers/SettingsHandler"; @@ -117,8 +117,8 @@ export default class SettingsStore { // We also maintain a list of monitors which are special watchers: they cause dispatches // when the setting changes. We track which rooms we're monitoring though to ensure we // don't duplicate updates on the bus. - private static watchers = {}; // { callbackRef => { callbackFn } } - private static monitors = {}; // { settingName => { roomId => callbackRef } } + private static watchers = new Map(); + private static monitors = new Map>(); // { settingName => { roomId => callbackRef } } // Counter used for generation of watcher IDs private static watcherCount = 1; @@ -163,7 +163,7 @@ export default class SettingsStore { callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue); }; - SettingsStore.watchers[watcherId] = localizedCallback; + SettingsStore.watchers.set(watcherId, localizedCallback); defaultWatchManager.watchSetting(settingName, roomId, localizedCallback); return watcherId; @@ -176,13 +176,13 @@ export default class SettingsStore { * to cancel. */ public static unwatchSetting(watcherReference: string) { - if (!SettingsStore.watchers[watcherReference]) { + if (!SettingsStore.watchers.has(watcherReference)) { console.warn(`Ending non-existent watcher ID ${watcherReference}`); return; } - defaultWatchManager.unwatchSetting(SettingsStore.watchers[watcherReference]); - delete SettingsStore.watchers[watcherReference]; + defaultWatchManager.unwatchSetting(SettingsStore.watchers.get(watcherReference)); + SettingsStore.watchers.delete(watcherReference); } /** @@ -196,10 +196,10 @@ export default class SettingsStore { public static monitorSetting(settingName: string, roomId: string) { roomId = roomId || null; // the thing wants null specifically to work, so appease it. - if (!this.monitors[settingName]) this.monitors[settingName] = {}; + if (!this.monitors.has(settingName)) this.monitors.set(settingName, new Map()); const registerWatcher = () => { - this.monitors[settingName][roomId] = SettingsStore.watchSetting( + this.monitors.get(settingName).set(roomId, SettingsStore.watchSetting( settingName, roomId, (settingName, inRoomId, level, newValueAtLevel, newValue) => { dis.dispatch({ action: 'setting_updated', @@ -210,19 +210,20 @@ export default class SettingsStore { newValue, }); }, - ); + )); }; - const hasRoom = Object.keys(this.monitors[settingName]).find((r) => r === roomId || r === null); + const rooms = Array.from(this.monitors.get(settingName).keys()); + const hasRoom = rooms.find((r) => r === roomId || r === null); if (!hasRoom) { registerWatcher(); } else { if (roomId === null) { // Unregister all existing watchers and register the new one - for (const roomId of Object.keys(this.monitors[settingName])) { - SettingsStore.unwatchSetting(this.monitors[settingName][roomId]); - } - this.monitors[settingName] = {}; + rooms.forEach(roomId => { + SettingsStore.unwatchSetting(this.monitors.get(settingName).get(roomId)); + }); + this.monitors.get(settingName).clear(); registerWatcher(); } // else a watcher is already registered for the room, so don't bother registering it again } @@ -257,6 +258,15 @@ export default class SettingsStore { return SETTINGS[settingName].isFeature; } + public static getBetaInfo(settingName: string) { + // consider a beta disabled if the config is explicitly set to false, in which case treat as normal Labs flag + if (SettingsStore.isFeature(settingName) + && SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, null, true, true) !== false + ) { + return SETTINGS[settingName]?.betaInfo; + } + } + /** * Determines if a setting is enabled. * If a setting is disabled then it should be hidden from the user. @@ -445,8 +455,8 @@ export default class SettingsStore { throw new Error("Setting '" + settingName + "' does not appear to be a setting."); } - // When features are specified in the config.json, we force them as enabled or disabled. - if (SettingsStore.isFeature(settingName)) { + // When non-beta features are specified in the config.json, we force them as enabled or disabled. + if (SettingsStore.isFeature(settingName) && !SETTINGS[settingName]?.betaInfo) { const configVal = SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, roomId, true, true); if (configVal === true || configVal === false) return false; } diff --git a/src/settings/WatchManager.ts b/src/settings/WatchManager.ts index ea2f158ef6..56f911f180 100644 --- a/src/settings/WatchManager.ts +++ b/src/settings/WatchManager.ts @@ -18,11 +18,7 @@ import { SettingLevel } from "./SettingLevel"; export type CallbackFn = (changedInRoomId: string, atLevel: SettingLevel, newValAtLevel: any) => void; -const IRRELEVANT_ROOM: string = null; - -interface RoomWatcherMap { - [roomId: string]: CallbackFn[]; -} +const IRRELEVANT_ROOM = Symbol("irrelevant-room"); /** * Generalized management class for dealing with watchers on a per-handler (per-level) @@ -30,25 +26,25 @@ interface RoomWatcherMap { * class, which are then proxied outwards to any applicable watchers. */ export class WatchManager { - private watchers: {[settingName: string]: RoomWatcherMap} = {}; + private watchers = new Map>(); // settingName -> roomId -> CallbackFn[] // Proxy for handlers to delegate changes to this manager public watchSetting(settingName: string, roomId: string | null, cb: CallbackFn) { - if (!this.watchers[settingName]) this.watchers[settingName] = {}; - if (!this.watchers[settingName][roomId]) this.watchers[settingName][roomId] = []; - this.watchers[settingName][roomId].push(cb); + if (!this.watchers.has(settingName)) this.watchers.set(settingName, new Map()); + if (!this.watchers.get(settingName).has(roomId)) this.watchers.get(settingName).set(roomId, []); + this.watchers.get(settingName).get(roomId).push(cb); } // Proxy for handlers to delegate changes to this manager public unwatchSetting(cb: CallbackFn) { - for (const settingName of Object.keys(this.watchers)) { - for (const roomId of Object.keys(this.watchers[settingName])) { + this.watchers.forEach((map) => { + map.forEach((callbacks) => { let idx; - while ((idx = this.watchers[settingName][roomId].indexOf(cb)) !== -1) { - this.watchers[settingName][roomId].splice(idx, 1); + while ((idx = callbacks.indexOf(cb)) !== -1) { + callbacks.splice(idx, 1); } - } - } + }); + }); } public notifyUpdate(settingName: string, inRoomId: string | null, atLevel: SettingLevel, newValueAtLevel: any) { @@ -56,21 +52,21 @@ export class WatchManager { // we also don't have a reliable way to get the old value of a setting. Instead, we'll just // let it fall through regardless and let the receiver dedupe if they want to. - if (!this.watchers[settingName]) return; + if (!this.watchers.has(settingName)) return; - const roomWatchers = this.watchers[settingName]; + const roomWatchers = this.watchers.get(settingName); const callbacks = []; - if (inRoomId !== null && roomWatchers[inRoomId]) { - callbacks.push(...roomWatchers[inRoomId]); + if (inRoomId !== null && roomWatchers.has(inRoomId)) { + callbacks.push(...roomWatchers.get(inRoomId)); } if (!inRoomId) { - // Fire updates to all the individual room watchers too, as they probably - // care about the change higher up. - callbacks.push(...Object.values(roomWatchers).flat(1)); - } else if (roomWatchers[IRRELEVANT_ROOM]) { - callbacks.push(...roomWatchers[IRRELEVANT_ROOM]); + // Fire updates to all the individual room watchers too, as they probably care about the change higher up. + const callbacks = Array.from(roomWatchers.values()).flat(1); + callbacks.push(...callbacks); + } else if (roomWatchers.has(IRRELEVANT_ROOM)) { + callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM)); } for (const callback of callbacks) { diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts index 92e094c83b..023845c9ee 100644 --- a/src/stores/CommunityPrototypeStore.ts +++ b/src/stores/CommunityPrototypeStore.ts @@ -126,7 +126,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { if (membership === EffectiveMembership.Invite) { try { const path = utils.encodeUri("/rooms/$roomId/group_info", {$roomId: room.roomId}); - const profile = await this.matrixClient._http.authedRequest( + const profile = await this.matrixClient.http.authedRequest( undefined, "GET", path, undefined, undefined, {prefix: "/_matrix/client/unstable/im.vector.custom"}); diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 53d07d0452..23254b98ab 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -65,6 +65,10 @@ class FlairStore extends EventEmitter { delete this._userGroups[userId]; } + cachedPublicisedGroups(userId) { + return this._userGroups[userId]; + } + getPublicisedGroupsCached(matrixClient, userId) { if (this._userGroups[userId]) { return Promise.resolve(this._userGroups[userId]); diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts index aea78c7460..d62f6c6110 100644 --- a/src/stores/RightPanelStorePhases.ts +++ b/src/stores/RightPanelStorePhases.ts @@ -24,6 +24,7 @@ export enum RightPanelPhases { EncryptionPanel = 'EncryptionPanel', RoomSummary = 'RoomSummary', Widget = 'Widget', + PinnedMessages = "PinnedMessages", Room3pidMemberInfo = 'Room3pidMemberInfo', // Group stuff @@ -43,6 +44,7 @@ export enum RightPanelPhases { export const RIGHT_PANEL_PHASES_NO_ARGS = [ RightPanelPhases.RoomSummary, RightPanelPhases.NotificationPanel, + RightPanelPhases.PinnedMessages, RightPanelPhases.FilePanel, RightPanelPhases.RoomMemberList, RightPanelPhases.GroupMemberList, diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index fe2e0a66b2..4ce1c789a5 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -17,17 +17,18 @@ limitations under the License. */ import React from "react"; -import {Store} from 'flux/utils'; -import {MatrixError} from "matrix-js-sdk/src/http-api"; +import { Store } from 'flux/utils'; +import { MatrixError } from "matrix-js-sdk/src/http-api"; import dis from '../dispatcher/dispatcher'; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { MatrixClientPeg } from '../MatrixClientPeg'; import * as sdk from '../index'; import Modal from '../Modal'; import { _t } from '../languageHandler'; import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache'; -import {ActionPayload} from "../dispatcher/payloads"; -import {retry} from "../utils/promise"; +import { ActionPayload } from "../dispatcher/payloads"; +import { Action } from "../dispatcher/actions"; +import { retry } from "../utils/promise"; import CountlyAnalytics from "../CountlyAnalytics"; const NUM_JOIN_RETRY = 5; @@ -136,13 +137,13 @@ class RoomViewStore extends Store { break; // join_room: // - opts: options for joinRoom - case 'join_room': + case Action.JoinRoom: this.joinRoom(payload); break; - case 'join_room_error': + case Action.JoinRoomError: this.joinRoomError(payload); break; - case 'join_room_ready': + case Action.JoinRoomReady: this.setState({ shouldPeek: false }); break; case 'on_client_not_viable': @@ -217,7 +218,11 @@ class RoomViewStore extends Store { this.setState(newState); if (payload.auto_join) { - this.joinRoom(payload); + dis.dispatch({ + ...payload, + action: Action.JoinRoom, + roomId: payload.room_id, + }); } } else if (payload.room_alias) { // Try the room alias to room ID navigation cache first to avoid @@ -298,41 +303,16 @@ class RoomViewStore extends Store { // We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not // have come down the sync stream yet, and that's the point at which we'd consider the user joined to the // room. - dis.dispatch({ action: 'join_room_ready' }); + dis.dispatch({ + action: Action.JoinRoomReady, + roomId: this.state.roomId, + }); } catch (err) { dis.dispatch({ - action: 'join_room_error', + action: Action.JoinRoomError, + roomId: this.state.roomId, err: err, }); - - let msg = err.message ? err.message : JSON.stringify(err); - console.log("Failed to join room:", msg); - - if (err.name === "ConnectionError") { - msg = _t("There was an error joining the room"); - } else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') { - msg =
    - {_t("Sorry, your homeserver is too old to participate in this room.")}
    - {_t("Please contact your homeserver administrator.")} -
    ; - } else if (err.httpStatus === 404) { - const invitingUserId = this.getInvitingUserId(this.state.roomId); - // only provide a better error message for invites - if (invitingUserId) { - // if the inviting user is on the same HS, there can only be one cause: they left. - if (invitingUserId.endsWith(`:${MatrixClientPeg.get().getDomain()}`)) { - msg = _t("The person who invited you already left the room."); - } else { - msg = _t("The person who invited you already left the room, or their server is offline."); - } - } - } - - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, { - title: _t("Failed to join room"), - description: msg, - }); } } @@ -351,6 +331,35 @@ class RoomViewStore extends Store { joining: false, joinError: payload.err, }); + const err = payload.err; + let msg = err.message ? err.message : JSON.stringify(err); + console.log("Failed to join room:", msg); + + if (err.name === "ConnectionError") { + msg = _t("There was an error joining the room"); + } else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') { + msg =
    + {_t("Sorry, your homeserver is too old to participate in this room.")}
    + {_t("Please contact your homeserver administrator.")} +
    ; + } else if (err.httpStatus === 404) { + const invitingUserId = this.getInvitingUserId(this.state.roomId); + // only provide a better error message for invites + if (invitingUserId) { + // if the inviting user is on the same HS, there can only be one cause: they left. + if (invitingUserId.endsWith(`:${MatrixClientPeg.get().getDomain()}`)) { + msg = _t("The person who invited you already left the room."); + } else { + msg = _t("The person who invited you already left the room, or their server is offline."); + } + } + } + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, { + title: _t("Failed to join room"), + description: msg, + }); } public reset() { diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 5f0054ff24..b768ae69df 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -196,7 +196,7 @@ export class SetupEncryptionStore extends EventEmitter { this.phase = PHASE_FINISHED; this.emit("update"); // async - ask other clients for keys, if necessary - MatrixClientPeg.get()._crypto.cancelAndResendAllOutgoingKeyRequests(); + MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests(); } async _setActiveVerificationRequest(request) { diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 4423891c61..40997d30a8 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -31,28 +31,27 @@ import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateS import {DefaultTagID} from "./room-list/models"; import {EnhancedMap, mapDiff} from "../utils/maps"; import {setHasDiff} from "../utils/sets"; -import {objectDiff} from "../utils/objects"; -import {arrayHasDiff} from "../utils/arrays"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import RoomViewStore from "./RoomViewStore"; -type SpaceKey = string | symbol; - interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; -export const HOME_SPACE = Symbol("home-space"); export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); -// Space Room ID/HOME_SPACE will be emitted when a Space's children change +// Space Room ID will be emitted when a Space's children change + +export interface ISuggestedRoom extends ISpaceSummaryRoom { + viaServers: string[]; +} const MAX_SUGGESTED_ROOMS = 20; -const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "home_space"}`; +const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`; const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] return arr.reduce((result, room: Room) => { @@ -67,7 +66,7 @@ export const getOrder = (order: string, creationTs: number, roomId: string): Arr if (typeof order === "string" && Array.from(order).every((c: string) => { const charCode = c.charCodeAt(0); - return charCode >= 0x20 && charCode <= 0x7F; + return charCode >= 0x20 && charCode <= 0x7E; })) { validatedOrder = order; } @@ -86,17 +85,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // The spaces representing the roots of the various tree-like hierarchies private rootSpaces: Room[] = []; - // The list of rooms not present in any currently joined spaces - private orphanedRooms = new Set(); // Map from room ID to set of spaces which list it as a child private parentMap = new EnhancedMap>(); - // Map from space key to SpaceNotificationState instance representing that space - private notificationStateMap = new Map(); + // Map from spaceId to SpaceNotificationState instance representing that space + private notificationStateMap = new Map(); // Map from space key to Set of room IDs that should be shown as part of that space's filter - private spaceFilteredRooms = new Map>(); - // The space currently selected in the Space Panel - if null then `Home` is selected + private spaceFilteredRooms = new Map>(); + // The space currently selected in the Space Panel - if null then All Rooms is selected private _activeSpace?: Room = null; - private _suggestedRooms: ISpaceSummaryRoom[] = []; + private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); public get invitedSpaces(): Room[] { @@ -111,7 +108,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._activeSpace || null; } - public get suggestedRooms(): ISpaceSummaryRoom[] { + public get suggestedRooms(): ISuggestedRoom[] { return this._suggestedRooms; } @@ -165,31 +162,41 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } if (space) { - const data = await this.fetchSuggestedRooms(space); + const suggestedRooms = await this.fetchSuggestedRooms(space); if (this._activeSpace === space) { - this._suggestedRooms = data.rooms.filter(roomInfo => { - return roomInfo.room_type !== RoomType.Space - && this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join"; - }); + this._suggestedRooms = suggestedRooms; this.emit(SUGGESTED_ROOMS, this._suggestedRooms); } } } - public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS) => { + public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS): Promise => { try { const data: { rooms: ISpaceSummaryRoom[]; events: ISpaceSummaryEvent[]; } = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, limit); - return data; + + const viaMap = new EnhancedMap>(); + data.events.forEach(ev => { + if (ev.type === EventType.SpaceChild && ev.content.via?.length) { + ev.content.via.forEach(via => { + viaMap.getOrCreate(ev.state_key, new Set()).add(via); + }); + } + }); + + return data.rooms.filter(roomInfo => { + return roomInfo.room_type !== RoomType.Space + && this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join"; + }).map(roomInfo => ({ + ...roomInfo, + viaServers: Array.from(viaMap.get(roomInfo.room_id) || []), + })); } catch (e) { console.error(e); } - return { - rooms: [], - events: [], - }; + return []; }; public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) { @@ -229,7 +236,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return room?.currentState.getStateEvents(EventType.SpaceParent) .filter(ev => { const content = ev.getContent(); - if (!content?.via) return false; + if (!content?.via?.length) return false; // TODO apply permissions check to verify that the parent mapping is valid if (canonicalOnly && !content?.canonical) return false; return true; @@ -244,7 +251,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public getSpaceFilteredRoomIds = (space: Room | null): Set => { - return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); + if (!space) { + return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); + } + return this.spaceFilteredRooms.get(space.roomId) || new Set(); }; private rebuild = throttle(() => { @@ -275,7 +285,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); }); - const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren)); + const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren)); // somewhat algorithm to handle full-cycles const detachedNodes = new Set(spaces); @@ -316,7 +326,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // rootSpaces.push(space); // }); - this.orphanedRooms = new Set(orphanedRooms); this.rootSpaces = rootSpaces; this.parentMap = backrefs; @@ -337,25 +346,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.rebuild(); } - private showInHomeSpace = (room: Room) => { - if (room.isSpaceRoom()) return false; - return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space - || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space - || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites - }; - - // Update a given room due to its tag changing (e.g DM-ness or Fav-ness) - // This can only change whether it shows up in the HOME_SPACE or not - private onRoomUpdate = (room: Room) => { - if (this.showInHomeSpace(room)) { - this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId); - this.emit(HOME_SPACE); - } else if (!this.orphanedRooms.has(room.roomId)) { - this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId); - this.emit(HOME_SPACE); - } - }; - private onSpaceMembersChange = (ev: MatrixEvent) => { // skip this update if we do not have a DM with this user if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return; @@ -369,16 +359,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldFilteredRooms = this.spaceFilteredRooms; this.spaceFilteredRooms = new Map(); - // put all room invites in the Home Space - const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); - this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); - - visibleRooms.forEach(room => { - if (this.showInHomeSpace(room)) { - this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId); - } - }); - this.rootSpaces.forEach(s => { // traverse each space tree in DFS to build up the supersets as you go up, // reusing results from like subtrees. @@ -395,7 +375,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const space = this.matrixClient?.getRoom(spaceId); // Add relevant DMs - space?.getJoinedMembers().forEach(member => { + space?.getMembers().forEach(member => { + if (member.membership !== "join" && member.membership !== "invite") return; DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => { roomIds.add(roomId); }); @@ -425,13 +406,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Update NotificationStates this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { if (roomIds.has(room.roomId)) { - // Don't aggregate notifications for DMs except in the Home Space - if (s !== HOME_SPACE) { - return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) - || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); - } - - return true; + return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) + || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); } return false; @@ -513,8 +489,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // TODO confirm this after implementing parenting behaviour if (room.isSpaceRoom()) { this.onSpaceUpdate(); - } else { - this.onRoomUpdate(room); } this.emit(room.roomId); break; @@ -527,38 +501,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; - private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => { - if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) { - // If the room was in favourites and now isn't or the opposite then update its position in the trees - const oldTags = lastEvent?.getContent()?.tags || {}; - const newTags = ev.getContent()?.tags || {}; - if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) { - this.onRoomUpdate(room); - } - } - } - - private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => { - if (ev.getType() === EventType.Direct) { - const lastContent = lastEvent.getContent(); - const content = ev.getContent(); - - const diff = objectDiff>(lastContent, content); - // filter out keys which changed by reference only by checking whether the sets differ - const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k])); - // DM tag changes, refresh relevant rooms - new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => { - const room = this.matrixClient?.getRoom(roomId); - if (room) { - this.onRoomUpdate(room); - } - }); - } - }; - protected async reset() { this.rootSpaces = []; - this.orphanedRooms = new Set(); this.parentMap = new EnhancedMap(); this.notificationStateMap = new Map(); this.spaceFilteredRooms = new Map(); @@ -573,8 +517,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.removeListener("Room", this.onRoom); this.matrixClient.removeListener("Room.myMembership", this.onRoom); this.matrixClient.removeListener("RoomState.events", this.onRoomState); - this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); - this.matrixClient.removeListener("accountData", this.onAccountData); } await this.reset(); } @@ -584,8 +526,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.on("Room", this.onRoom); this.matrixClient.on("Room.myMembership", this.onRoom); this.matrixClient.on("RoomState.events", this.onRoomState); - this.matrixClient.on("Room.accountData", this.onRoomAccountData); - this.matrixClient.on("accountData", this.onAccountData); await this.onSpaceUpdate(); // trigger an initial update @@ -610,7 +550,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Don't context switch when navigating to the space room // as it will cause you to end up in the wrong room this.setActiveSpace(room, false); - } else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) { + } else if (this.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) { this.switchToRelatedSpace(roomId); } @@ -628,7 +568,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } } - public getNotificationState(key: SpaceKey): SpaceNotificationState { + public getNotificationState(key: string): SpaceNotificationState { if (this.notificationStateMap.has(key)) { return this.notificationStateMap.get(key); } diff --git a/src/stores/SpaceTreeLevelLayoutStore.ts b/src/stores/SpaceTreeLevelLayoutStore.ts new file mode 100644 index 0000000000..424e9f4012 --- /dev/null +++ b/src/stores/SpaceTreeLevelLayoutStore.ts @@ -0,0 +1,48 @@ +/* +Copyright 2021 Šimon Brandner + +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. +*/ + +const getSpaceCollapsedKey = (roomId: string, parents: Set): string => { + const separator = "/"; + let path = ""; + if (parents) { + for (const entry of parents.entries()) { + path += entry + separator; + } + } + return `mx_space_collapsed_${path + roomId}`; +}; + +export default class SpaceTreeLevelLayoutStore { + private static internalInstance: SpaceTreeLevelLayoutStore; + + public static get instance(): SpaceTreeLevelLayoutStore { + if (!SpaceTreeLevelLayoutStore.internalInstance) { + SpaceTreeLevelLayoutStore.internalInstance = new SpaceTreeLevelLayoutStore(); + } + return SpaceTreeLevelLayoutStore.internalInstance; + } + + public setSpaceCollapsedState(roomId: string, parents: Set, collapsed: boolean) { + // XXX: localStorage doesn't allow booleans + localStorage.setItem(getSpaceCollapsedKey(roomId, parents), collapsed.toString()); + } + + public getSpaceCollapsedState(roomId: string, parents: Set, fallback: boolean): boolean { + const collapsedLocalStorage = localStorage.getItem(getSpaceCollapsedKey(roomId, parents)); + // XXX: localStorage doesn't allow booleans + return collapsedLocalStorage ? collapsedLocalStorage === "true" : fallback; + } +} diff --git a/src/stores/UIStore.ts b/src/stores/UIStore.ts new file mode 100644 index 0000000000..7511527eb0 --- /dev/null +++ b/src/stores/UIStore.ts @@ -0,0 +1,112 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import EventEmitter from "events"; +import ResizeObserver from 'resize-observer-polyfill'; +import ResizeObserverEntry from 'resize-observer-polyfill/src/ResizeObserverEntry'; + +export enum UI_EVENTS { + Resize = "resize" +} + +export type ResizeObserverCallbackFunction = (entries: ResizeObserverEntry[]) => void; + +export default class UIStore extends EventEmitter { + private static _instance: UIStore = null; + + private resizeObserver: ResizeObserver; + + private uiElementDimensions = new Map(); + private trackedUiElements = new Map(); + + public windowWidth: number; + public windowHeight: number; + + constructor() { + super(); + + // eslint-disable-next-line no-restricted-properties + this.windowWidth = window.innerWidth; + // eslint-disable-next-line no-restricted-properties + this.windowHeight = window.innerHeight; + + this.resizeObserver = new ResizeObserver(this.resizeObserverCallback); + this.resizeObserver.observe(document.body); + } + + public static get instance(): UIStore { + if (!UIStore._instance) { + UIStore._instance = new UIStore(); + } + return UIStore._instance; + } + + public static destroy(): void { + if (UIStore._instance) { + UIStore._instance.resizeObserver.disconnect(); + UIStore._instance.removeAllListeners(); + UIStore._instance = null; + } + } + + public getElementDimensions(name: string): DOMRectReadOnly { + return this.uiElementDimensions.get(name); + } + + public trackElementDimensions(name: string, element: Element): void { + this.trackedUiElements.set(element, name); + this.resizeObserver.observe(element); + } + + public stopTrackingElementDimensions(name: string): void { + let trackedElement: Element; + this.trackedUiElements.forEach((trackedElementName, element) => { + if (trackedElementName === name) { + trackedElement = element; + } + }); + if (trackedElement) { + this.resizeObserver.unobserve(trackedElement); + this.uiElementDimensions.delete(name); + this.trackedUiElements.delete(trackedElement); + } + } + + public isTrackingElementDimensions(name: string): boolean { + return this.uiElementDimensions.has(name); + } + + private resizeObserverCallback = (entries: ResizeObserverEntry[]) => { + const windowEntry = entries.find(entry => entry.target === document.body); + + if (windowEntry) { + this.windowWidth = windowEntry.contentRect.width; + this.windowHeight = windowEntry.contentRect.height; + } + + entries.forEach(entry => { + const trackedElementName = this.trackedUiElements.get(entry.target); + if (trackedElementName) { + this.uiElementDimensions.set(trackedElementName, entry.contentRect); + this.emit(trackedElementName, UI_EVENTS.Resize, entry); + } + }); + + this.emit(UI_EVENTS.Resize, entries); + } +} + +window.mxUIStore = UIStore.instance; diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index 1da0e661e8..f5b9d9bc6a 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -94,10 +94,10 @@ export class MessagePreviewStore extends AsyncStoreWithClient { * @param inTagId The tag ID in which the room resides * @returns The preview, or null if none present. */ - public getPreviewForRoom(room: Room, inTagId: TagID): string { + public async getPreviewForRoom(room: Room, inTagId: TagID): Promise { if (!room) return null; // invalid room, just return nothing - if (!this.previews.has(room.roomId)) this.generatePreview(room, inTagId); + if (!this.previews.has(room.roomId)) await this.generatePreview(room, inTagId); const previews = this.previews.get(room.roomId); if (!previews) return null; @@ -108,7 +108,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient { return previews.get(inTagId); } - private generatePreview(room: Room, tagId?: TagID) { + private async generatePreview(room: Room, tagId?: TagID) { const events = room.timeline; if (!events) return; // should only happen in tests @@ -130,6 +130,9 @@ export class MessagePreviewStore extends AsyncStoreWithClient { } const event = events[i]; + + await this.matrixClient.decryptEventIfNeeded(event); + const previewDef = PREVIEWS[event.getType()]; if (!previewDef) continue; if (previewDef.isState && isNullOrUndefined(event.getStateKey())) continue; @@ -173,8 +176,9 @@ export class MessagePreviewStore extends AsyncStoreWithClient { if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') { const event = payload.event; // TODO: Type out the dispatcher - if (!this.previews.has(event.getRoomId())) return; // not important - this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY); + const isHistoricalEvent = payload.hasOwnProperty("isLiveEvent") && !payload.isLiveEvent + if (!this.previews.has(event.getRoomId()) || isHistoricalEvent) return; // not important + await this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY); } } } diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 58eb6ed317..b5961f1ac3 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -426,8 +426,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient { return; // don't do anything on rooms that aren't visible } - if (cause === RoomUpdateCause.NewRoom && !this.prefilterConditions.every(c => c.isVisible(room))) { - return; // don't do anything on new rooms which ought not to be shown + if ((cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.PossibleTagChange) && + !this.prefilterConditions.every(c => c.isVisible(room)) + ) { + return; // don't do anything on new/moved rooms which ought not to be shown } const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); @@ -680,7 +682,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { promise = this.recalculatePrefiltering(); } else { this.filterConditions.push(filter); - // Runtime filters with spaces disable prefiltering for the search all spaces effect + // Runtime filters with spaces disable prefiltering for the search all spaces feature if (SettingsStore.getValue("feature_spaces")) { // this has to be awaited so that `setKnownRooms` is called in time for the `addFilterCondition` below // this way the runtime filters are only evaluated on one dataset and not both. @@ -712,10 +714,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient { if (this.algorithm) { this.algorithm.removeFilterCondition(filter); - // Runtime filters with spaces disable prefiltering for the search all spaces effect - if (SettingsStore.getValue("feature_spaces")) { - promise = this.recalculatePrefiltering(); - } + } + // Runtime filters with spaces disable prefiltering for the search all spaces feature + if (SettingsStore.getValue("feature_spaces")) { + promise = this.recalculatePrefiltering(); } } idx = this.prefilterConditions.indexOf(filter); diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts index 13e1d83901..0b1b78bc75 100644 --- a/src/stores/room-list/SpaceWatcher.ts +++ b/src/stores/room-list/SpaceWatcher.ts @@ -24,26 +24,34 @@ import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore"; * Watches for changes in spaces to manage the filter on the provided RoomListStore */ export class SpaceWatcher { - private filter = new SpaceFilterCondition(); + private filter: SpaceFilterCondition; private activeSpace: Room = SpaceStore.instance.activeSpace; constructor(private store: RoomListStoreClass) { - this.updateFilter(); // get the filter into a consistent state - store.addFilter(this.filter); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated); } - private onSelectedSpaceUpdated = (activeSpace: Room) => { + private onSelectedSpaceUpdated = (activeSpace?: Room) => { this.activeSpace = activeSpace; - this.updateFilter(); + + if (this.filter) { + if (activeSpace) { + this.updateFilter(); + } else { + this.store.removeFilter(this.filter); + this.filter = null; + } + } else if (activeSpace) { + this.filter = new SpaceFilterCondition(); + this.updateFilter(); + this.store.addFilter(this.filter); + } }; private updateFilter = () => { - if (this.activeSpace) { - SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { - this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); - }); - } + SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { + this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); + }); this.filter.updateSpace(this.activeSpace); }; } diff --git a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts index d909fb6288..b016a4256c 100644 --- a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts @@ -17,6 +17,7 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { TagID } from "../../models"; import { IAlgorithm } from "./IAlgorithm"; +import { compare } from "../../../../utils/strings"; /** * Sorts rooms according to the browser's determination of alphabetic. @@ -24,7 +25,7 @@ import { IAlgorithm } from "./IAlgorithm"; export class AlphabeticAlgorithm implements IAlgorithm { public async sortRooms(rooms: Room[], tagId: TagID): Promise { return rooms.sort((a, b) => { - return a.name.localeCompare(b.name); + return compare(a.name, b.name); }); } } diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 8e63c23131..7ec91a3249 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -17,7 +17,7 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { EventEmitter } from "events"; -import { removeHiddenChars } from "matrix-js-sdk/src/utils"; +import { normalize } from "matrix-js-sdk/src/utils"; import { throttle } from "lodash"; /** @@ -62,20 +62,10 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio if (!room.name) return false; // should realistically not happen: the js-sdk always calculates a name - return this.matches(room.name); + return this.matches(room.normalizedName); } - private normalize(val: string): string { - // Note: we have to match the filter with the removeHiddenChars() room name because the - // function strips spaces and other characters (M becomes RN for example, in lowercase). - return removeHiddenChars(val.toLowerCase()) - // Strip all punctuation - .replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "") - // We also doubly convert to lowercase to work around oddities of the library. - .toLowerCase(); - } - - public matches(val: string): boolean { - return this.normalize(val).includes(this.normalize(this.search)); + public matches(normalizedName: string): boolean { + return normalizedName.includes(normalize(this.search)); } } diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts index 43bdcb3879..6a06bee0d8 100644 --- a/src/stores/room-list/filters/SpaceFilterCondition.ts +++ b/src/stores/room-list/filters/SpaceFilterCondition.ts @@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { IDestroyable } from "../../../utils/IDestroyable"; -import SpaceStore, {HOME_SPACE} from "../../SpaceStore"; +import SpaceStore from "../../SpaceStore"; import { setHasDiff } from "../../../utils/sets"; /** @@ -55,10 +55,12 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi } }; - private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE; + private getSpaceEventKey = (space: Room) => space.roomId; public updateSpace(space: Room) { - SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); + if (this.space) { + SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); + } SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate); this.onStoreUpdate(); // initial update from the change to the space } diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 17371d6d45..397d637125 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2020, 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,6 +52,8 @@ import {getCustomTheme} from "../../theme"; import CountlyAnalytics from "../../CountlyAnalytics"; import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { ELEMENT_CLIENT_ID } from "../../identifiers"; +import { getUserLanguage } from "../../languageHandler"; // TODO: Destroy all of this code @@ -194,6 +196,9 @@ export class StopGapWidget extends EventEmitter { currentUserId: MatrixClientPeg.get().getUserId(), userDisplayName: OwnProfileStore.instance.displayName, userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(), + clientId: ELEMENT_CLIENT_ID, + clientTheme: SettingsStore.getValue("theme"), + clientLanguage: getUserLanguage(), }, opts?.asPopout); const parsed = new URL(templated); @@ -395,6 +400,7 @@ export class StopGapWidget extends EventEmitter { } private onEvent = (ev: MatrixEvent) => { + MatrixClientPeg.get().decryptEventIfNeeded(ev); if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; if (ev.getRoomId() !== this.eventListenerRoomId) return; this.feedEvent(ev); diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 8a286d909b..25e81c47a2 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -44,6 +44,7 @@ import { CHAT_EFFECTS } from "../../effects"; import { containsEmoji } from "../../effects/utils"; import dis from "../../dispatcher/dispatcher"; import {tryTransformPermalinkToLocalHref} from "../../utils/permalinks/Permalinks"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; // TODO: Purge this from the universe @@ -144,6 +145,52 @@ export class StopGapWidgetDriver extends WidgetDriver { return {roomId, eventId: r.event_id}; } + public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise { + limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice + + const client = MatrixClientPeg.get(); + const roomId = ActiveRoomObserver.activeRoomId; + const room = client.getRoom(roomId); + if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client"); + + const results: MatrixEvent[] = []; + const events = room.getLiveTimeline().getEvents(); // timelines are most recent last + for (let i = events.length - 1; i > 0; i--) { + if (results.length >= limit) break; + + const ev = events[i]; + if (ev.getType() !== eventType) continue; + if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue; + results.push(ev); + } + + return results.map(e => e.event); + } + + public async readStateEvents( + eventType: string, stateKey: string | undefined, limit: number, + ): Promise { + limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice + + const client = MatrixClientPeg.get(); + const roomId = ActiveRoomObserver.activeRoomId; + const room = client.getRoom(roomId); + if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client"); + + const results: MatrixEvent[] = []; + const state = room.currentState.events.get(eventType); + if (state) { + if (stateKey === "" || !!stateKey) { + const forKey = state.get(stateKey); + if (forKey) results.push(forKey); + } else { + results.push(...Array.from(state.values())); + } + } + + return results.slice(0, limit).map(e => e.event); + } + public async askOpenID(observer: SimpleObservable) { const oidcState = WidgetPermissionStore.instance.getOIDCState( this.forWidget, this.forWidgetKind, this.inRoomId, diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index e6ef534202..f5734d74c5 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -25,6 +25,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SettingLevel } from "../../settings/SettingLevel"; import { arrayFastClone } from "../../utils/arrays"; import { UPDATE_EVENT } from "../AsyncStore"; +import { compare } from "../../utils/strings"; export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout"; @@ -240,7 +241,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { if (orderA === orderB) { // We just need a tiebreak - return a.id.localeCompare(b.id); + return compare(a.id, b.id); } return orderA - orderB; diff --git a/src/utils/DecryptFile.ts b/src/utils/DecryptFile.ts index 93cedbc707..d073393170 100644 --- a/src/utils/DecryptFile.ts +++ b/src/utils/DecryptFile.ts @@ -17,63 +17,8 @@ limitations under the License. // Pull in the encryption lib so that we can decrypt attachments. import encrypt from 'browser-encrypt-attachment'; import {mediaFromContent} from "../customisations/Media"; -import {IEncryptedFile} from "../customisations/models/IMediaEventContent"; - -// WARNING: We have to be very careful about what mime-types we allow into blobs, -// as for performance reasons these are now rendered via URL.createObjectURL() -// rather than by converting into data: URIs. -// -// This means that the content is rendered using the origin of the script which -// called createObjectURL(), and so if the content contains any scripting then it -// will pose a XSS vulnerability when the browser renders it. This is particularly -// bad if the user right-clicks the URI and pastes it into a new window or tab, -// as the blob will then execute with access to Element's full JS environment(!) -// -// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647 -// for details. -// -// We mitigate this by only allowing mime-types into blobs which we know don't -// contain any scripting, and instantiate all others as application/octet-stream -// regardless of what mime-type the event claimed. Even if the payload itself -// is some malicious HTML, the fact we instantiate it with a media mimetype or -// application/octet-stream means the browser doesn't try to render it as such. -// -// One interesting edge case is image/svg+xml, which empirically *is* rendered -// correctly if the blob is set to the src attribute of an img tag (for thumbnails) -// *even if the mimetype is application/octet-stream*. However, empirically JS -// in the SVG isn't executed in this scenario, so we seem to be okay. -// -// Tested on Chrome 65 and Firefox 60 -// -// The list below is taken mainly from -// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats -// N.B. Matrix doesn't currently specify which mimetypes are valid in given -// events, so we pick the ones which HTML5 browsers should be able to display -// -// For the record, mime-types which must NEVER enter this list below include: -// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar. - -const ALLOWED_BLOB_MIMETYPES = [ - 'image/jpeg', - 'image/gif', - 'image/png', - - 'video/mp4', - 'video/webm', - 'video/ogg', - - 'audio/mp4', - 'audio/webm', - 'audio/aac', - 'audio/mpeg', - 'audio/ogg', - 'audio/wave', - 'audio/wav', - 'audio/x-wav', - 'audio/x-pn-wav', - 'audio/flac', - 'audio/x-flac', -]; +import { IEncryptedFile } from "../customisations/models/IMediaEventContent"; +import { getBlobSafeMimeType } from "./blobs"; /** * Decrypt a file attached to a matrix event. @@ -100,9 +45,7 @@ export function decryptFile(file: IEncryptedFile): Promise { // browser (e.g. by copying the URI into a new tab or window.) // See warning at top of file. let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : ''; - if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) { - mimetype = 'application/octet-stream'; - } + mimetype = getBlobSafeMimeType(mimetype); return new Blob([dataArray], {type: mimetype}); }); diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.js index fd12a454f6..4d46d10f6c 100644 --- a/src/utils/ResizeNotifier.js +++ b/src/utils/ResizeNotifier.js @@ -74,12 +74,6 @@ export default class ResizeNotifier extends EventEmitter { // can be called in quick succession notifyWindowResized() { - // no need to throttle this one, - // also it could make scrollbars appear for - // a split second when the room list manual layout is now - // taller than the available space - this.emit("leftPanelResized"); - this._updateMiddlePanel(); } } diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index 1e130bd605..e527f43c29 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {percentageOf, percentageWithin} from "./numbers"; + /** * Quickly resample an array to have less/more data points. If an input which is larger * than the desired size is provided, it will be downsampled. Similarly, if the input @@ -27,7 +29,7 @@ export function arrayFastResample(input: number[], points: number): number[] { // Heavily inspired by matrix-media-repo (used with permission) // https://github.com/turt2live/matrix-media-repo/blob/abe72c87d2e29/util/util_audio/fastsample.go#L10 - let samples: number[] = []; + const samples: number[] = []; if (input.length > points) { // Danger: this loop can cause out of memory conditions if the input is too small. const everyNth = Math.round(input.length / points); @@ -44,17 +46,63 @@ export function arrayFastResample(input: number[], points: number): number[] { } } - // Sanity fill, just in case - while (samples.length < points) { - samples.push(input[input.length - 1]); - } + // Trim to size & return + return arrayTrimFill(samples, points, arraySeed(input[input.length - 1], points)); +} - // Sanity trim, just in case - if (samples.length > points) { - samples = samples.slice(0, points); - } +/** + * Attempts a smooth resample of the given array. This is functionally similar to arrayFastResample + * though can take longer due to the smoothing of data. + * @param {number[]} input The input array to resample. + * @param {number} points The number of samples to end up with. + * @returns {number[]} The resampled array. + */ +export function arraySmoothingResample(input: number[], points: number): number[] { + if (input.length === points) return input; // short-circuit a complicated call - return samples; + let samples: number[] = []; + if (input.length > points) { + // We're downsampling. To preserve the curve we'll actually reduce our sample + // selection and average some points between them. + + // All we're doing here is repeatedly averaging the waveform down to near our + // target value. We don't average down to exactly our target as the loop might + // never end, and we can over-average the data. Instead, we'll get as far as + // we can and do a followup fast resample (the neighbouring points will be close + // to the actual waveform, so we can get away with this safely). + while (samples.length > (points * 2) || samples.length === 0) { + samples = []; + for (let i = 1; i < input.length - 1; i += 2) { + const prevPoint = input[i - 1]; + const nextPoint = input[i + 1]; + const currPoint = input[i]; + const average = (prevPoint + nextPoint + currPoint) / 3; + samples.push(average); + } + input = samples; + } + + return arrayFastResample(samples, points); + } else { + // In practice there's not much purpose in burning CPU for short arrays only to + // end up with a result that can't possibly look much different than the fast + // resample, so just skip ahead to the fast resample. + return arrayFastResample(input, points); + } +} + +/** + * Rescales the input array to have values that are inclusively within the provided + * minimum and maximum. + * @param {number[]} input The array to rescale. + * @param {number} newMin The minimum value to scale to. + * @param {number} newMax The maximum value to scale to. + * @returns {number[]} The rescaled array. + */ +export function arrayRescale(input: number[], newMin: number, newMax: number): number[] { + const min: number = Math.min(...input); + const max: number = Math.max(...input); + return input.map(v => percentageWithin(percentageOf(v, min, max), newMin, newMax)); } /** diff --git a/src/utils/blobs.ts b/src/utils/blobs.ts new file mode 100644 index 0000000000..4e073a3936 --- /dev/null +++ b/src/utils/blobs.ts @@ -0,0 +1,78 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// WARNING: We have to be very careful about what mime-types we allow into blobs, +// as for performance reasons these are now rendered via URL.createObjectURL() +// rather than by converting into data: URIs. +// +// This means that the content is rendered using the origin of the script which +// called createObjectURL(), and so if the content contains any scripting then it +// will pose a XSS vulnerability when the browser renders it. This is particularly +// bad if the user right-clicks the URI and pastes it into a new window or tab, +// as the blob will then execute with access to Element's full JS environment(!) +// +// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647 +// for details. +// +// We mitigate this by only allowing mime-types into blobs which we know don't +// contain any scripting, and instantiate all others as application/octet-stream +// regardless of what mime-type the event claimed. Even if the payload itself +// is some malicious HTML, the fact we instantiate it with a media mimetype or +// application/octet-stream means the browser doesn't try to render it as such. +// +// One interesting edge case is image/svg+xml, which empirically *is* rendered +// correctly if the blob is set to the src attribute of an img tag (for thumbnails) +// *even if the mimetype is application/octet-stream*. However, empirically JS +// in the SVG isn't executed in this scenario, so we seem to be okay. +// +// Tested on Chrome 65 and Firefox 60 +// +// The list below is taken mainly from +// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats +// N.B. Matrix doesn't currently specify which mimetypes are valid in given +// events, so we pick the ones which HTML5 browsers should be able to display +// +// For the record, mime-types which must NEVER enter this list below include: +// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar. + +const ALLOWED_BLOB_MIMETYPES = [ + 'image/jpeg', + 'image/gif', + 'image/png', + + 'video/mp4', + 'video/webm', + 'video/ogg', + + 'audio/mp4', + 'audio/webm', + 'audio/aac', + 'audio/mpeg', + 'audio/ogg', + 'audio/wave', + 'audio/wav', + 'audio/x-wav', + 'audio/x-pn-wav', + 'audio/flac', + 'audio/x-flac', +]; + +export function getBlobSafeMimeType(mimetype: string): string { + if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) { + return 'application/octet-stream'; + } + return mimetype; +} diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index 015ecca22e..d87c826cc2 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -346,9 +346,14 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string { return permalink; } - const m = decodeURIComponent(permalink).match(matrixLinkify.ELEMENT_URL_PATTERN); - if (m) { - return m[1]; + try { + const m = decodeURIComponent(permalink).match(matrixLinkify.ELEMENT_URL_PATTERN); + if (m) { + return m[1]; + } + } catch (e) { + // Not a valid URI + return permalink; } // A bit of a hack to convert permalinks of unknown origin to Element links diff --git a/src/utils/promise.ts b/src/utils/promise.ts index f828ddfdaf..4ebbb27141 100644 --- a/src/utils/promise.ts +++ b/src/utils/promise.ts @@ -51,24 +51,6 @@ export function defer(): IDeferred { return {resolve, reject, promise}; } -// Promise.allSettled polyfill until browser support is stable in Firefox -export function allSettled(promises: Promise[]): Promise | ISettledRejected>> { - if (Promise.allSettled) { - return Promise.allSettled(promises); - } - - // @ts-ignore - typescript isn't smart enough to see the disjoint here - return Promise.all(promises.map((promise) => { - return promise.then(value => ({ - status: "fulfilled", - value, - })).catch(reason => ({ - status: "rejected", - reason, - })); - })); -} - // Helper method to retry a Promise a given number of times or until a predicate fails export async function retry(fn: () => Promise, num: number, predicate?: (e: E) => boolean) { let lastErr: E; diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 5856682445..beb9f31ddd 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -73,3 +73,14 @@ export function copyNode(ref: Element): boolean { selectText(ref); return document.execCommand('copy'); } + + +const collator = new Intl.Collator(); +/** + * Performant language-sensitive string comparison + * @param a the first string to compare + * @param b the second string to compare + */ +export function compare(a: string, b: string): number { + return collator.compare(a, b); +} diff --git a/src/verification.js b/src/verification.ts similarity index 62% rename from src/verification.js rename to src/verification.ts index 74e3897d3a..acd9f6d2b2 100644 --- a/src/verification.js +++ b/src/verification.ts @@ -1,5 +1,5 @@ /* -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,16 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from './MatrixClientPeg'; +import { User } from "matrix-js-sdk/src/models/user"; + +import { MatrixClientPeg } from './MatrixClientPeg'; import dis from "./dispatcher/dispatcher"; import Modal from './Modal'; import * as sdk from './index'; -import { _t } from './languageHandler'; -import {RightPanelPhases} from "./stores/RightPanelStorePhases"; -import {findDMForUser} from './createRoom'; -import {accessSecretStorage} from './SecurityManager'; -import {verificationMethods} from 'matrix-js-sdk/src/crypto'; -import {Action} from './dispatcher/actions'; +import { RightPanelPhases } from "./stores/RightPanelStorePhases"; +import { findDMForUser } from './createRoom'; +import { accessSecretStorage } from './SecurityManager'; +import { verificationMethods } from 'matrix-js-sdk/src/crypto'; +import { Action } from './dispatcher/actions'; +import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog"; +import {IDevice} from "./components/views/right_panel/UserInfo"; async function enable4SIfNeeded() { const cli = MatrixClientPeg.get(); @@ -39,40 +42,7 @@ async function enable4SIfNeeded() { return true; } -function UntrustedDeviceDialog(props) { - const {device, user, onFinished} = props; - const BaseDialog = sdk.getComponent("dialogs.BaseDialog"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - let askToVerifyText; - let newSessionText; - - if (MatrixClientPeg.get().getUserId() === user.userId) { - newSessionText = _t("You signed in to a new session without verifying it:"); - askToVerifyText = _t("Verify your other session using one of the options below."); - } else { - newSessionText = _t("%(name)s (%(userId)s) signed in to a new session without verifying it:", - {name: user.displayName, userId: user.userId}); - askToVerifyText = _t("Ask this user to verify their session, or manually verify it below."); - } - - return -
    -

    {newSessionText}

    -

    {device.getDisplayName()} ({device.deviceId})

    -

    {askToVerifyText}

    -
    -
    - onFinished("legacy")}>{_t("Manually Verify by Text")} - onFinished("sas")}>{_t("Interactively verify by Emoji")} - onFinished()}>{_t("Done")} -
    -
    ; -} - -export async function verifyDevice(user, device) { +export async function verifyDevice(user: User, device: IDevice) { const cli = MatrixClientPeg.get(); if (cli.isGuest()) { dis.dispatch({action: 'require_registration'}); @@ -115,7 +85,7 @@ export async function verifyDevice(user, device) { }); } -export async function legacyVerifyUser(user) { +export async function legacyVerifyUser(user: User) { const cli = MatrixClientPeg.get(); if (cli.isGuest()) { dis.dispatch({action: 'require_registration'}); @@ -135,7 +105,7 @@ export async function legacyVerifyUser(user) { }); } -export async function verifyUser(user) { +export async function verifyUser(user: User) { const cli = MatrixClientPeg.get(); if (cli.isGuest()) { dis.dispatch({action: 'require_registration'}); @@ -155,7 +125,7 @@ export async function verifyUser(user) { }); } -export function pendingVerificationRequestForUser(user) { +export function pendingVerificationRequestForUser(user: User) { const cli = MatrixClientPeg.get(); const dmRoom = findDMForUser(cli, user.userId); if (dmRoom) { diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index caa5241e1a..61da435151 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -15,12 +15,13 @@ limitations under the License. */ import EventEmitter from "events"; -import {UPDATE_EVENT} from "../stores/AsyncStore"; -import {arrayFastResample, arraySeed} from "../utils/arrays"; -import {SimpleObservable} from "matrix-widget-api"; -import {IDestroyable} from "../utils/IDestroyable"; -import {PlaybackClock} from "./PlaybackClock"; -import {clamp} from "../utils/numbers"; +import { UPDATE_EVENT } from "../stores/AsyncStore"; +import { arrayFastResample, arrayRescale, arraySeed, arraySmoothingResample } from "../utils/arrays"; +import { SimpleObservable } from "matrix-widget-api"; +import { IDestroyable } from "../utils/IDestroyable"; +import { PlaybackClock } from "./PlaybackClock"; +import { createAudioContext, decodeOgg } from "./compat"; +import { clamp } from "../utils/numbers"; export enum PlaybackState { Decoding = "decoding", @@ -32,6 +33,23 @@ export enum PlaybackState { export const PLAYBACK_WAVEFORM_SAMPLES = 39; const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); +function makePlaybackWaveform(input: number[]): number[] { + // First, convert negative amplitudes to positive so we don't detect zero as "noisy". + const noiseWaveform = input.map(v => Math.abs(v)); + + // Next, we'll resample the waveform using a smoothing approach so we can keep the same rough shape. + // We also rescale the waveform to be 0-1 for the remaining function logic. + const resampled = arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1); + + // Then, we'll do a high and low pass filter to isolate actual speaking volumes within the rescaled + // waveform. Most speech happens below the 0.5 mark. + const filtered = resampled.map(v => clamp(v, 0.1, 0.5)); + + // Finally, we'll rescale the filtered waveform (0.1-0.5 becomes 0-1 again) so the user sees something + // sensible. This is what we return to keep our contract of "values between zero and one". + return arrayRescale(filtered, 0, 1); +} + export class Playback extends EventEmitter implements IDestroyable { private readonly context: AudioContext; private source: AudioBufferSourceNode; @@ -49,12 +67,16 @@ export class Playback extends EventEmitter implements IDestroyable { */ constructor(private buf: ArrayBuffer, seedWaveform = DEFAULT_WAVEFORM) { super(); - this.context = new AudioContext(); + this.context = createAudioContext(); this.resampledWaveform = arrayFastResample(seedWaveform ?? DEFAULT_WAVEFORM, PLAYBACK_WAVEFORM_SAMPLES); this.waveformObservable.update(this.resampledWaveform); this.clock = new PlaybackClock(this.context); } + /** + * Stable waveform for the playback. Values are guaranteed to be between + * zero and one, inclusive. + */ public get waveform(): number[] { return this.resampledWaveform; } @@ -91,15 +113,32 @@ export class Playback extends EventEmitter implements IDestroyable { } public async prepare() { - this.audioBuf = await this.context.decodeAudioData(this.buf); + // Safari compat: promise API not supported on this function + this.audioBuf = await new Promise((resolve, reject) => { + this.context.decodeAudioData(this.buf, b => resolve(b), async e => { + // This error handler is largely for Safari as well, which doesn't support Opus/Ogg + // very well. + console.error("Error decoding recording: ", e); + console.warn("Trying to re-encode to WAV instead..."); + + const wav = await decodeOgg(this.buf); + + // noinspection ES6MissingAwait - not needed when using callbacks + this.context.decodeAudioData(wav, b => resolve(b), e => { + console.error("Still failed to decode recording: ", e); + reject(e); + }); + }); + }); // Update the waveform to the real waveform once we have channel data to use. We don't // exactly trust the user-provided waveform to be accurate... - const waveform = Array.from(this.audioBuf.getChannelData(0)).map(v => clamp(v, 0, 1)); - this.resampledWaveform = arrayFastResample(waveform, PLAYBACK_WAVEFORM_SAMPLES); + const waveform = Array.from(this.audioBuf.getChannelData(0)); + this.resampledWaveform = makePlaybackWaveform(waveform); this.waveformObservable.update(this.resampledWaveform); this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore + this.clock.flagLoadTime(); // must happen first because setting the duration fires a clock update this.clock.durationSeconds = this.audioBuf.duration; } diff --git a/src/voice/PlaybackClock.ts b/src/voice/PlaybackClock.ts index 06d6381691..d6d36e861f 100644 --- a/src/voice/PlaybackClock.ts +++ b/src/voice/PlaybackClock.ts @@ -54,6 +54,15 @@ export class PlaybackClock implements IDestroyable { } }; + /** + * Mark the time in the audio context where the clip starts/has been loaded. + * This is to ensure the clock isn't skewed into thinking it is ~0.5s into + * a clip when the duration is set. + */ + public flagLoadTime() { + this.clipStart = this.context.currentTime; + } + public flagStart() { if (this.stopped) { this.clipStart = this.context.currentTime; diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index c4a0a78ce5..fde5779fa2 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -19,16 +19,17 @@ import encoderPath from 'opus-recorder/dist/encoderWorker.min.js'; import {MatrixClient} from "matrix-js-sdk/src/client"; import CallMediaHandler from "../CallMediaHandler"; import {SimpleObservable} from "matrix-widget-api"; -import {clamp} from "../utils/numbers"; +import {clamp, percentageOf, percentageWithin} from "../utils/numbers"; import EventEmitter from "events"; import {IDestroyable} from "../utils/IDestroyable"; import {Singleflight} from "../utils/Singleflight"; import {PayloadEvent, WORKLET_NAME} from "./consts"; import {UPDATE_EVENT} from "../stores/AsyncStore"; import {Playback} from "./Playback"; +import {createAudioContext} from "./compat"; const CHANNELS = 1; // stereo isn't important -const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. +export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. const BITRATE = 24000; // 24kbps is pretty high quality for our use case in opus. const TARGET_MAX_LENGTH = 120; // 2 minutes in seconds. Somewhat arbitrary, though longer == larger files. const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary. @@ -55,6 +56,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private recorderStream: MediaStream; private recorderFFT: AnalyserNode; private recorderWorklet: AudioWorkletNode; + private recorderProcessor: ScriptProcessorNode; private buffer = new Uint8Array(0); // use this.audioBuffer to access private mxc: string; private recording = false; @@ -90,78 +92,107 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { } private async makeRecorder() { - this.recorderStream = await navigator.mediaDevices.getUserMedia({ - audio: { - channelCount: CHANNELS, - noiseSuppression: true, // browsers ignore constraints they can't honour - deviceId: CallMediaHandler.getAudioInput(), - }, - }); - this.recorderContext = new AudioContext({ - // latencyHint: "interactive", // we don't want a latency hint (this causes data smoothing) - }); - this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream); - this.recorderFFT = this.recorderContext.createAnalyser(); + try { + this.recorderStream = await navigator.mediaDevices.getUserMedia({ + audio: { + channelCount: CHANNELS, + noiseSuppression: true, // browsers ignore constraints they can't honour + deviceId: CallMediaHandler.getAudioInput(), + }, + }); + this.recorderContext = createAudioContext({ + // latencyHint: "interactive", // we don't want a latency hint (this causes data smoothing) + }); + this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream); + this.recorderFFT = this.recorderContext.createAnalyser(); - // Bring the FFT time domain down a bit. The default is 2048, and this must be a power - // of two. We use 64 points because we happen to know down the line we need less than - // that, but 32 would be too few. Large numbers are not helpful here and do not add - // precision: they introduce higher precision outputs of the FFT (frequency data), but - // it makes the time domain less than helpful. - this.recorderFFT.fftSize = 64; + // Bring the FFT time domain down a bit. The default is 2048, and this must be a power + // of two. We use 64 points because we happen to know down the line we need less than + // that, but 32 would be too few. Large numbers are not helpful here and do not add + // precision: they introduce higher precision outputs of the FFT (frequency data), but + // it makes the time domain less than helpful. + this.recorderFFT.fftSize = 64; - // Set up our worklet. We use this for timing information and waveform analysis: the - // web audio API prefers this be done async to avoid holding the main thread with math. - const mxRecorderWorkletPath = document.body.dataset.vectorRecorderWorkletScript; - if (!mxRecorderWorkletPath) { - throw new Error("Unable to create recorder: no worklet script registered"); - } - await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath); - this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME); - - // Connect our inputs and outputs - this.recorderSource.connect(this.recorderFFT); - this.recorderSource.connect(this.recorderWorklet); - this.recorderWorklet.connect(this.recorderContext.destination); - - // Dev note: we can't use `addEventListener` for some reason. It just doesn't work. - this.recorderWorklet.port.onmessage = (ev) => { - switch (ev.data['ev']) { - case PayloadEvent.Timekeep: - this.processAudioUpdate(ev.data['timeSeconds']); - break; - case PayloadEvent.AmplitudeMark: - // Sanity check to make sure we're adding about one sample per second - if (ev.data['forSecond'] === this.amplitudes.length) { - this.amplitudes.push(ev.data['amplitude']); - } - break; + // Set up our worklet. We use this for timing information and waveform analysis: the + // web audio API prefers this be done async to avoid holding the main thread with math. + const mxRecorderWorkletPath = document.body.dataset.vectorRecorderWorkletScript; + if (!mxRecorderWorkletPath) { + // noinspection ExceptionCaughtLocallyJS + throw new Error("Unable to create recorder: no worklet script registered"); } - }; - this.recorder = new Recorder({ - encoderPath, // magic from webpack - encoderSampleRate: SAMPLE_RATE, - encoderApplication: 2048, // voice (default is "audio") - streamPages: true, // this speeds up the encoding process by using CPU over time - encoderFrameSize: 20, // ms, arbitrary frame size we send to the encoder - numberOfChannels: CHANNELS, - sourceNode: this.recorderSource, - encoderBitRate: BITRATE, + // Connect our inputs and outputs + this.recorderSource.connect(this.recorderFFT); - // We use low values for the following to ease CPU usage - the resulting waveform - // is indistinguishable for a voice message. Note that the underlying library will - // pick defaults which prefer the highest possible quality, CPU be damned. - encoderComplexity: 3, // 0-10, 10 is slow and high quality. - resampleQuality: 3, // 0-10, 10 is slow and high quality - }); - this.recorder.ondataavailable = (a: ArrayBuffer) => { - const buf = new Uint8Array(a); - const newBuf = new Uint8Array(this.buffer.length + buf.length); - newBuf.set(this.buffer, 0); - newBuf.set(buf, this.buffer.length); - this.buffer = newBuf; - }; + if (this.recorderContext.audioWorklet) { + await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath); + this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME); + this.recorderSource.connect(this.recorderWorklet); + this.recorderWorklet.connect(this.recorderContext.destination); + + // Dev note: we can't use `addEventListener` for some reason. It just doesn't work. + this.recorderWorklet.port.onmessage = (ev) => { + switch (ev.data['ev']) { + case PayloadEvent.Timekeep: + this.processAudioUpdate(ev.data['timeSeconds']); + break; + case PayloadEvent.AmplitudeMark: + // Sanity check to make sure we're adding about one sample per second + if (ev.data['forSecond'] === this.amplitudes.length) { + this.amplitudes.push(ev.data['amplitude']); + } + break; + } + }; + } else { + // Safari fallback: use a processor node instead, buffered to 1024 bytes of data + // like the worklet is. + this.recorderProcessor = this.recorderContext.createScriptProcessor(1024, CHANNELS, CHANNELS); + this.recorderSource.connect(this.recorderProcessor); + this.recorderProcessor.connect(this.recorderContext.destination); + this.recorderProcessor.addEventListener("audioprocess", this.onAudioProcess); + } + + this.recorder = new Recorder({ + encoderPath, // magic from webpack + encoderSampleRate: SAMPLE_RATE, + encoderApplication: 2048, // voice (default is "audio") + streamPages: true, // this speeds up the encoding process by using CPU over time + encoderFrameSize: 20, // ms, arbitrary frame size we send to the encoder + numberOfChannels: CHANNELS, + sourceNode: this.recorderSource, + encoderBitRate: BITRATE, + + // We use low values for the following to ease CPU usage - the resulting waveform + // is indistinguishable for a voice message. Note that the underlying library will + // pick defaults which prefer the highest possible quality, CPU be damned. + encoderComplexity: 3, // 0-10, 10 is slow and high quality. + resampleQuality: 3, // 0-10, 10 is slow and high quality + }); + this.recorder.ondataavailable = (a: ArrayBuffer) => { + const buf = new Uint8Array(a); + const newBuf = new Uint8Array(this.buffer.length + buf.length); + newBuf.set(this.buffer, 0); + newBuf.set(buf, this.buffer.length); + this.buffer = newBuf; + }; + } catch (e) { + console.error("Error starting recording: ", e); + if (e instanceof DOMException) { // Unhelpful DOMExceptions are common - parse them sanely + console.error(`${e.name} (${e.code}): ${e.message}`); + } + + // Clean up as best as possible + if (this.recorderStream) this.recorderStream.getTracks().forEach(t => t.stop()); + if (this.recorderSource) this.recorderSource.disconnect(); + if (this.recorder) this.recorder.close(); + if (this.recorderContext) { + // noinspection ES6MissingAwait - not important that we wait + this.recorderContext.close(); + } + + throw e; // rethrow so upstream can handle it + } } private get audioBuffer(): Uint8Array { @@ -190,6 +221,13 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { return this.mxc; } + private onAudioProcess = (ev: AudioProcessingEvent) => { + this.processAudioUpdate(ev.playbackTime); + + // We skip the functionality of the worklet regarding waveform calculations: we + // should get that information pretty quick during the playback info. + }; + private processAudioUpdate = (timeSeconds: number) => { if (!this.recording) return; @@ -197,7 +235,16 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { // size. The time domain is also known as the audio waveform. We're ignoring the // output of the FFT here (frequency data) because we're not interested in it. const data = new Float32Array(this.recorderFFT.fftSize); - this.recorderFFT.getFloatTimeDomainData(data); + if (!this.recorderFFT.getFloatTimeDomainData) { + // Safari compat + const data2 = new Uint8Array(this.recorderFFT.fftSize); + this.recorderFFT.getByteTimeDomainData(data2); + for (let i = 0; i < data2.length; i++) { + data[i] = percentageWithin(percentageOf(data2[i], 0, 256), -1, 1); + } + } else { + this.recorderFFT.getFloatTimeDomainData(data); + } // We can't just `Array.from()` the array because we're dealing with 32bit floats // and the built-in function won't consider that when converting between numbers. @@ -268,7 +315,11 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { // Disconnect the source early to start shutting down resources await this.recorder.stop(); // stop first to flush the last frame this.recorderSource.disconnect(); - this.recorderWorklet.disconnect(); + if (this.recorderWorklet) this.recorderWorklet.disconnect(); + if (this.recorderProcessor) { + this.recorderProcessor.disconnect(); + this.recorderProcessor.removeEventListener("audioprocess", this.onAudioProcess); + } // close the context after the recorder so the recorder doesn't try to // connect anything to the context (this would generate a warning) diff --git a/src/voice/compat.ts b/src/voice/compat.ts new file mode 100644 index 0000000000..316d779e28 --- /dev/null +++ b/src/voice/compat.ts @@ -0,0 +1,82 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {SAMPLE_RATE} from "./VoiceRecording"; + +// @ts-ignore - we know that this is not a module. We're looking for a path. +import decoderWasmPath from 'opus-recorder/dist/decoderWorker.min.wasm'; +import wavEncoderPath from 'opus-recorder/dist/waveWorker.min.js'; +import decoderPath from 'opus-recorder/dist/decoderWorker.min.js'; + +export function createAudioContext(opts?: AudioContextOptions): AudioContext { + if (window.AudioContext) { + return new AudioContext(opts); + } else if (window.webkitAudioContext) { + // While the linter is correct that "a constructor name should not start with + // a lowercase letter", it's also wrong to think that we have control over this. + // eslint-disable-next-line new-cap + return new window.webkitAudioContext(opts); + } else { + throw new Error("Unsupported browser"); + } +} + +export function decodeOgg(audioBuffer: ArrayBuffer): Promise { + // Condensed version of decoder example, using a promise: + // https://github.com/chris-rudmin/opus-recorder/blob/master/example/decoder.html + return new Promise((resolve) => { // no reject because the workers don't seem to have a fail path + console.log("Decoder WASM path: " + decoderWasmPath); // so we use the variable (avoid tree shake) + const typedArray = new Uint8Array(audioBuffer); + const decoderWorker = new Worker(decoderPath); + const wavWorker = new Worker(wavEncoderPath); + + decoderWorker.postMessage({ + command: 'init', + decoderSampleRate: SAMPLE_RATE, + outputBufferSampleRate: SAMPLE_RATE, + }); + + wavWorker.postMessage({ + command: 'init', + wavBitDepth: 24, // standard for 48khz (SAMPLE_RATE) + wavSampleRate: SAMPLE_RATE, + }); + + decoderWorker.onmessage = (ev) => { + if (ev.data === null) { // null == done + wavWorker.postMessage({command: 'done'}); + return; + } + + wavWorker.postMessage({ + command: 'encode', + buffers: ev.data, + }, ev.data.map(b => b.buffer)); + }; + + wavWorker.onmessage = (ev) => { + if (ev.data.message === 'page') { + // The encoding comes through as a single page + resolve(new Blob([ev.data.page], {type: "audio/wav"}).arrayBuffer()); + } + }; + + decoderWorker.postMessage({ + command: 'decode', + pages: typedArray, + }, [typedArray.buffer]); + }); +} diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index 273d22dc81..05e6c59083 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -96,6 +96,16 @@ export class CapabilityText { [EventDirection.Receive]: _td("See when the avatar changes in your active room"), }, }, + [EventType.RoomMember]: { + [WidgetKind.Room]: { + [EventDirection.Send]: _td("Kick, ban, or invite people to this room, and make you leave"), + [EventDirection.Receive]: _td("See when people join, leave, or are invited to this room"), + }, + [GENERIC_WIDGET_KIND]: { + [EventDirection.Send]: _td("Kick, ban, or invite people to your active room, and make you leave"), + [EventDirection.Receive]: _td("See when people join, leave, or are invited to your active room"), + }, + }, }; private static nonStateSendRecvCaps: ISendRecvStaticCapText = { diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index 1e3f92e788..12316ac01c 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -23,8 +23,10 @@ import dis from '../src/dispatcher/dispatcher'; import { CallEvent, CallState } from 'matrix-js-sdk/src/webrtc/call'; import DMRoomMap from '../src/utils/DMRoomMap'; import EventEmitter from 'events'; -import { Action } from '../src/dispatcher/actions'; import SdkConfig from '../src/SdkConfig'; +import { ActionPayload } from '../src/dispatcher/payloads'; +import { Actions } from '../src/notifications/types'; +import { Action } from '../src/dispatcher/actions'; const REAL_ROOM_ID = '$room1:example.org'; const MAPPED_ROOM_ID = '$room2:example.org'; @@ -75,6 +77,18 @@ class FakeCall extends EventEmitter { } } +function untilDispatch(waitForAction: string): Promise { + let dispatchHandle; + return new Promise(resolve => { + dispatchHandle = dis.register(payload => { + if (payload.action === waitForAction) { + dis.unregister(dispatchHandle); + resolve(payload); + } + }); + }); +} + describe('CallHandler', () => { let dmRoomMap; let callHandler; @@ -94,6 +108,21 @@ describe('CallHandler', () => { callHandler = new CallHandler(); callHandler.start(); + const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org'); + const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org'); + const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org'); + + MatrixClientPeg.get().getRoom = roomId => { + switch (roomId) { + case REAL_ROOM_ID: + return realRoom; + case MAPPED_ROOM_ID: + return mappedRoom; + case MAPPED_ROOM_ID_2: + return mappedRoom2; + } + }; + dmRoomMap = { getUserIdForRoomId: roomId => { if (roomId === REAL_ROOM_ID) { @@ -134,38 +163,34 @@ describe('CallHandler', () => { SdkConfig.unset(); }); + it('should look up the correct user and open the room when a phone number is dialled', async () => { + MatrixClientPeg.get().getThirdpartyUser = jest.fn().mockResolvedValue([{ + userid: '@user2:example.org', + protocol: "im.vector.protocol.sip_native", + fields: { + is_native: true, + lookup_success: true, + }, + }]); + + dis.dispatch({ + action: Action.DialNumber, + number: '01818118181', + }, true); + + const viewRoomPayload = await untilDispatch('view_room'); + expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID); + }); + it('should move calls between rooms when remote asserted identity changes', async () => { - const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org'); - const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org'); - const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org'); - - MatrixClientPeg.get().getRoom = roomId => { - switch (roomId) { - case REAL_ROOM_ID: - return realRoom; - case MAPPED_ROOM_ID: - return mappedRoom; - case MAPPED_ROOM_ID_2: - return mappedRoom2; - } - }; - dis.dispatch({ action: 'place_call', type: PlaceCallType.Voice, room_id: REAL_ROOM_ID, }, true); - let dispatchHandle; // wait for the call to be set up - await new Promise(resolve => { - dispatchHandle = dis.register(payload => { - if (payload.action === 'call_state') { - resolve(); - } - }); - }); - dis.unregister(dispatchHandle); + await untilDispatch('call_state'); // should start off in the actual room ID it's in at the protocol level expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall); diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 7347ff2658..5b466b4bb0 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -77,7 +77,7 @@ describe('MessagePanel', function() { DMRoomMap.makeShared(); }); - afterEach(function() { + afterEach(function () { clock.uninstall(); }); @@ -88,7 +88,21 @@ describe('MessagePanel', function() { events.push(test_utils.mkMessage( { event: true, room: "!room:id", user: "@user:id", - ts: ts0 + i*1000, + ts: ts0 + i * 1000, + })); + } + return events; + } + + // Just to avoid breaking Dateseparator tests that might run at 00hrs + function mkOneDayEvents() { + const events = []; + const ts0 = Date.parse('09 May 2004 00:12:00 GMT'); + for (let i = 0; i < 10; i++) { + events.push(test_utils.mkMessage( + { + event: true, room: "!room:id", user: "@user:id", + ts: ts0 + i * 1000, })); } return events; @@ -104,7 +118,7 @@ describe('MessagePanel', function() { let i = 0; events.push(test_utils.mkMessage({ event: true, room: "!room:id", user: "@user:id", - ts: ts0 + ++i*1000, + ts: ts0 + ++i * 1000, })); for (i = 0; i < 10; i++) { @@ -151,7 +165,7 @@ describe('MessagePanel', function() { }, getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', }, - ts: ts0 + i*1000, + ts: ts0 + i * 1000, mship: 'join', prevMship: 'join', name: 'A user', @@ -250,7 +264,6 @@ describe('MessagePanel', function() { }), ]; } - function isReadMarkerVisible(rmContainer) { return rmContainer && rmContainer.children.length > 0; } @@ -296,7 +309,7 @@ describe('MessagePanel', function() { const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container'); // it should follow the
  • which wraps the event tile for event 4 - const eventContainer = ReactDOM.findDOMNode(tiles[4]).parentNode; + const eventContainer = ReactDOM.findDOMNode(tiles[4]); expect(rm.previousSibling).toEqual(eventContainer); }); @@ -352,7 +365,7 @@ describe('MessagePanel', function() { const tiles = TestUtils.scryRenderedComponentsWithType( mp, sdk.getComponent('rooms.EventTile')); const tileContainers = tiles.map(function(t) { - return ReactDOM.findDOMNode(t).parentNode; + return ReactDOM.findDOMNode(t); }); // find the
  • which wraps the read marker @@ -437,4 +450,17 @@ describe('MessagePanel', function() { // read marker should be hidden given props and at the last event expect(isReadMarkerVisible(rm)).toBeFalsy(); }); + + it('should render Date separators for the events', function () { + const events = mkOneDayEvents(); + const res = mount( + , + ); + const Dates = res.find(sdk.getComponent('messages.DateSeparator')); + + expect(Dates.length).toEqual(1); + }); }); diff --git a/test/components/structures/auth/Login-test.js b/test/components/structures/auth/Login-test.js index 9b73137936..f39802464f 100644 --- a/test/components/structures/auth/Login-test.js +++ b/test/components/structures/auth/Login-test.js @@ -133,7 +133,7 @@ describe('Login', function() { root.setState({ flows: [{ "type": "m.login.sso", - "org.matrix.msc2858.identity_providers": [{ + "identity_providers": [{ id: "a", name: "Provider 1", }, { diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js index 093e5588d0..28fead770c 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.js @@ -9,6 +9,8 @@ import sdk from '../../../skinned-sdk'; import {Room, RoomMember, User} from 'matrix-js-sdk'; +import { compare } from "../../../../src/utils/strings"; + function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; } @@ -88,6 +90,7 @@ describe('MemberList', () => { }; memberListRoom.currentState = { members: {}, + getMember: jest.fn(), getStateEvents: (eventType, stateKey) => stateKey === undefined ? [] : null, // ignore 3pid invites }; for (const member of [...adminUsers, ...moderatorUsers, ...defaultUsers]) { @@ -172,7 +175,7 @@ describe('MemberList', () => { if (!groupChange) { const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; - const nameCompare = nameB.localeCompare(nameA); + const nameCompare = compare(nameB, nameA); console.log("Comparing name"); expect(nameCompare).toBeGreaterThanOrEqual(0); } else { diff --git a/test/end-to-end-tests/.gitignore b/test/end-to-end-tests/.gitignore index 61f9012393..9180d32e90 100644 --- a/test/end-to-end-tests/.gitignore +++ b/test/end-to-end-tests/.gitignore @@ -1,3 +1,4 @@ node_modules *.png element/env +performance-entries.json diff --git a/test/end-to-end-tests/src/session.js b/test/end-to-end-tests/src/session.js index 4c611ef877..6c68929a0b 100644 --- a/test/end-to-end-tests/src/session.js +++ b/test/end-to-end-tests/src/session.js @@ -208,7 +208,7 @@ module.exports = class ElementSession { this.log.done(); } - close() { + async close() { return this.browser.close(); } diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js index 234d60da9f..f29b485c84 100644 --- a/test/end-to-end-tests/start.js +++ b/test/end-to-end-tests/start.js @@ -79,8 +79,26 @@ async function runTests() { await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000)); } - await Promise.all(sessions.map((session) => session.close())); + const performanceEntries = {}; + await Promise.all(sessions.map(async (session) => { + // Collecting all performance monitoring data before closing the session + const measurements = await session.page.evaluate(() => { + let measurements = []; + window.mxPerformanceMonitor.addPerformanceDataCallback({ + entryNames: [ + window.mxPerformanceEntryNames.REGISTER, + ], + callback: (events) => { + measurements = JSON.stringify(events); + }, + }, true); + return measurements; + }); + performanceEntries[session.username] = JSON.parse(measurements); + return session.close(); + })); + fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries)); if (failure) { process.exit(-1); } else { diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index aef788647d..20c48c29db 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -101,6 +101,7 @@ const invite1 = "!invite1:server"; const invite2 = "!invite2:server"; const room1 = "!room1:server"; const room2 = "!room2:server"; +const room3 = "!room3:server"; const space1 = "!space1:server"; const space2 = "!space2:server"; const space3 = "!space3:server"; @@ -361,8 +362,8 @@ describe("SpaceStore", () => { expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy(); }); - it("home space does not contain rooms/low priority from rooms within spaces", () => { - expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy(); + it("home space does contain rooms/low priority even if they are also shown in a space", () => { + expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy(); }); it("space contains child rooms", () => { @@ -614,8 +615,8 @@ describe("SpaceStore", () => { describe("space auto switching tests", () => { beforeEach(async () => { - [room1, room2, orphan1].forEach(mkRoom); - mkSpace(space1, [room1, room2]); + [room1, room2, room3, orphan1].forEach(mkRoom); + mkSpace(space1, [room1, room2, room3]); mkSpace(space2, [room1, room2]); client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([ @@ -641,15 +642,15 @@ describe("SpaceStore", () => { it("switch to canonical parent space for room", async () => { viewRoom(room1); - await store.setActiveSpace(null, false); + await store.setActiveSpace(client.getRoom(space2), false); viewRoom(room2); expect(store.activeSpace).toBe(client.getRoom(space2)); }); it("switch to first containing space for room", async () => { viewRoom(room2); - await store.setActiveSpace(null, false); - viewRoom(room1); + await store.setActiveSpace(client.getRoom(space2), false); + viewRoom(room3); expect(store.activeSpace).toBe(client.getRoom(space1)); }); @@ -659,6 +660,13 @@ describe("SpaceStore", () => { viewRoom(orphan1); expect(store.activeSpace).toBeNull(); }); + + it("when switching rooms in the all rooms home space don't switch to related space", async () => { + viewRoom(room2); + await store.setActiveSpace(null, false); + viewRoom(room1); + expect(store.activeSpace).toBeNull(); + }); }); describe("traverseSpace", () => { diff --git a/test/test-utils.js b/test/test-utils.js index 6dc02463a5..b9014f4876 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -90,11 +90,12 @@ export function createTestClient() { }), // Used by various internal bits we aren't concerned with (yet) - _sessionStore: { + sessionStore: { store: { getItem: jest.fn(), }, }, + decryptEventIfNeeded: () => Promise.resolve(), }; } @@ -233,6 +234,7 @@ export function mkStubRoom(roomId = null) { }), getMembersWithMembership: jest.fn().mockReturnValue([]), getJoinedMembers: jest.fn().mockReturnValue([]), + getMembers: jest.fn().mockReturnValue([]), getPendingEvents: () => [], getLiveTimeline: () => stubTimeline, getUnfilteredTimelineSet: () => null, @@ -244,6 +246,7 @@ export function mkStubRoom(roomId = null) { maySendMessage: jest.fn().mockReturnValue(true), currentState: { getStateEvents: jest.fn(), + getMember: jest.fn(), mayClientSendStateEvent: jest.fn().mockReturnValue(true), maySendStateEvent: jest.fn().mockReturnValue(true), maySendEvent: jest.fn().mockReturnValue(true), diff --git a/test/utils/arrays-test.ts b/test/utils/arrays-test.ts index c5be59ab43..5974915965 100644 --- a/test/utils/arrays-test.ts +++ b/test/utils/arrays-test.ts @@ -21,7 +21,9 @@ import { arrayHasDiff, arrayHasOrderChange, arrayMerge, + arrayRescale, arraySeed, + arraySmoothingResample, arrayTrimFill, arrayUnion, ArrayUtil, @@ -29,9 +31,9 @@ import { } from "../../src/utils/arrays"; import {objectFromEntries} from "../../src/utils/objects"; -function expectSample(i: number, input: number[], expected: number[]) { +function expectSample(i: number, input: number[], expected: number[], smooth = false) { console.log(`Resample case index: ${i}`); // for debugging test failures - const result = arrayFastResample(input, expected.length); + const result = (smooth ? arraySmoothingResample : arrayFastResample)(input, expected.length); expect(result).toBeDefined(); expect(result).toHaveLength(expected.length); expect(result).toEqual(expected); @@ -65,6 +67,47 @@ describe('arrays', () => { }); }); + describe('arraySmoothingResample', () => { + it('should downsample', () => { + // Dev note: these aren't great samples, but they demonstrate the bare minimum. Ideally + // we'd be feeding a thousand values in and seeing what a curve of 250 values looks like, + // but that's not really feasible to manually verify accuracy. + [ + {input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3, 3]}, // Odd -> Even + {input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3]}, // Odd -> Odd + {input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3, 3]}, // Even -> Odd + {input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3]}, // Even -> Even + ].forEach((c, i) => expectSample(i, c.input, c.output, true)); + }); + + it('should upsample', () => { + [ + {input: [2, 0, 2], output: [2, 2, 0, 0, 2, 2]}, // Odd -> Even + {input: [2, 0, 2], output: [2, 2, 0, 0, 2]}, // Odd -> Odd + {input: [2, 0], output: [2, 2, 2, 0, 0]}, // Even -> Odd + {input: [2, 0], output: [2, 2, 2, 0, 0, 0]}, // Even -> Even + ].forEach((c, i) => expectSample(i, c.input, c.output, true)); + }); + + it('should maintain sample', () => { + [ + {input: [2, 0, 2], output: [2, 0, 2]}, // Odd + {input: [2, 0], output: [2, 0]}, // Even + ].forEach((c, i) => expectSample(i, c.input, c.output, true)); + }); + }); + + describe('arrayRescale', () => { + it('should rescale', () => { + const input = [8, 9, 1, 0, 2, 7, 10]; + const output = [80, 90, 10, 0, 20, 70, 100]; + const result = arrayRescale(input, 0, 100); + expect(result).toBeDefined(); + expect(result).toHaveLength(output.length); + expect(result).toEqual(output); + }); + }); + describe('arrayTrimFill', () => { it('should shrink arrays', () => { const input = [1, 2, 3]; diff --git a/yarn.lock b/yarn.lock index 8b4ac35d6a..0ff235a660 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1325,6 +1325,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz": + version "3.2.3" + resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": version "2.1.8-no-fsevents" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz#da7c3996b8e6e19ebd14d82eaced2313e7769f9b" @@ -1638,12 +1642,12 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@types/sanitize-html@^1.27.0": - version "1.27.0" - resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-1.27.0.tgz#77702dc856f16efecc005014c1d2e45b1f2cbc56" - integrity sha512-j7Vnh3P7W4ZcoRsHNO2HpwA2m1d0c2+l39xqSQqH0+WlfcvKypgZp45eCC7NJ75ZyXPxNb2PSbIL6LtZ6E0Qbw== +"@types/sanitize-html@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.3.1.tgz#094d696b83b7394b016e96342bbffa6a028795ce" + integrity sha512-+UT/XRluJuCunRftwO6OzG6WOBgJ+J3sROIoSJWX+7PB2FtTJTEJLrHCcNwzCQc0r60bej3WAbaigK+VZtZCGw== dependencies: - htmlparser2 "^4.1.0" + htmlparser2 "^6.0.0" "@types/stack-utils@^1.0.1": version "1.0.1" @@ -2401,29 +2405,29 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -cheerio-select-tmp@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646" - integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ== +cheerio-select@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.4.0.tgz#3a16f21e37a2ef0f211d6d1aa4eff054bb22cdc9" + integrity sha512-sobR3Yqz27L553Qa7cK6rtJlMDbiKPdNywtR95Sj/YgfpLfy0u6CGJuaBKe5YE/vTc23SCRKxWSdlon/w6I/Ew== dependencies: - css-select "^3.1.2" - css-what "^4.0.0" - domelementtype "^2.1.0" - domhandler "^4.0.0" - domutils "^2.4.4" + css-select "^4.1.2" + css-what "^5.0.0" + domelementtype "^2.2.0" + domhandler "^4.2.0" + domutils "^2.6.0" -cheerio@^1.0.0-rc.3, cheerio@^1.0.0-rc.5: - version "1.0.0-rc.5" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f" - integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw== +cheerio@^1.0.0-rc.3, cheerio@^1.0.0-rc.9: + version "1.0.0-rc.9" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.9.tgz#a3ae6b7ce7af80675302ff836f628e7cb786a67f" + integrity sha512-QF6XVdrLONO6DXRF5iaolY+odmhj2CLj+xzNod7INPWMi/x9X4SOylH0S/vaPpX+AUU6t04s34SQNh7DbkuCng== dependencies: - cheerio-select-tmp "^0.1.0" - dom-serializer "~1.2.0" - domhandler "^4.0.0" - entities "~2.1.0" - htmlparser2 "^6.0.0" - parse5 "^6.0.0" - parse5-htmlparser2-tree-adapter "^6.0.0" + cheerio-select "^1.4.0" + dom-serializer "^1.3.1" + domhandler "^4.2.0" + htmlparser2 "^6.1.0" + parse5 "^6.0.1" + parse5-htmlparser2-tree-adapter "^6.0.1" + tslib "^2.2.0" chokidar@^3.4.0, chokidar@^3.5.1: version "3.5.1" @@ -2705,21 +2709,21 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -css-select@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" - integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== +css-select@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286" + integrity sha512-nu5ye2Hg/4ISq4XqdLY2bEatAcLIdt3OYGFc9Tm9n7VSlFBcfRv0gBNksHRgSdUDQGtN3XrZ94ztW+NfzkFSUw== dependencies: boolbase "^1.0.0" - css-what "^4.0.0" - domhandler "^4.0.0" - domutils "^2.4.3" + css-what "^5.0.0" + domhandler "^4.2.0" + domutils "^2.6.0" nth-check "^2.0.0" -css-what@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" - integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== +css-what@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.0.tgz#f0bf4f8bac07582722346ab243f6a35b512cfc47" + integrity sha512-qxyKHQvgKwzwDWC/rGbT821eJalfupxYW2qbSJSAtdSTimsr/MlaGONoNLllaUPZWf8QnbcKM/kPVYUQuEKAFA== cssesc@^3.0.0: version "3.0.0" @@ -2925,9 +2929,9 @@ doctrine@^3.0.0: esutils "^2.0.2" dom-helpers@^5.0.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b" - integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== dependencies: "@babel/runtime" "^7.8.7" csstype "^3.0.2" @@ -2940,10 +2944,10 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -dom-serializer@^1.0.1, dom-serializer@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" - integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== +dom-serializer@^1.0.1, dom-serializer@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.1.tgz#d845a1565d7c041a95e5dab62184ab41e3a519be" + integrity sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q== dependencies: domelementtype "^2.0.1" domhandler "^4.0.0" @@ -2954,10 +2958,10 @@ domelementtype@1, domelementtype@^1.3.1: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@^2.0.1, domelementtype@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" - integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== domexception@^2.0.1: version "2.0.1" @@ -2973,19 +2977,12 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -domhandler@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" - integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== +domhandler@^4.0.0, domhandler@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" + integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== dependencies: - domelementtype "^2.0.1" - -domhandler@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" - integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== - dependencies: - domelementtype "^2.1.0" + domelementtype "^2.2.0" domutils@^1.5.1: version "1.7.0" @@ -2995,14 +2992,14 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" -domutils@^2.0.0, domutils@^2.4.3, domutils@^2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" - integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== +domutils@^2.4.4, domutils@^2.5.2, domutils@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7" + integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA== dependencies: dom-serializer "^1.0.1" - domelementtype "^2.0.1" - domhandler "^4.0.0" + domelementtype "^2.2.0" + domhandler "^4.2.0" ecc-jsbn@~0.1.1: version "0.1.2" @@ -3068,7 +3065,7 @@ entities@^1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== -entities@^2.0.0, entities@~2.1.0: +entities@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== @@ -4226,9 +4223,9 @@ hoist-non-react-statics@^3.3.0: react-is "^16.7.0" hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== hosted-git-info@^3.0.6: version "3.0.7" @@ -4278,16 +4275,6 @@ htmlparser2@^3.10.0: inherits "^2.0.1" readable-stream "^3.1.1" -htmlparser2@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" - integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== - dependencies: - domelementtype "^2.0.1" - domhandler "^3.0.0" - domutils "^2.0.0" - entities "^2.0.0" - htmlparser2@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" @@ -4298,6 +4285,16 @@ htmlparser2@^6.0.0: domutils "^2.4.4" entities "^2.0.0" +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -5677,8 +5674,8 @@ mathml-tag-names@^2.1.3: integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "10.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c8f69c0b7937b9064938c134d708c4d064b71315" + version "11.1.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/acb9bc8cc5234326a7583514a8e120a4ac42eedc" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" @@ -5711,10 +5708,10 @@ matrix-react-test-utils@^0.2.2: "@babel/traverse" "^7.13.17" walk "^2.3.14" -matrix-widget-api@^0.1.0-beta.13: - version "0.1.0-beta.13" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.13.tgz#ebddc83eaef39bbb87b621a02a35902e1a29b9ef" - integrity sha512-DJAvuX2E7gxc/a9rtJPDh17ba9xGIOAoBHcWirNTN3KGodzsrZ+Ns+M/BREFWMwGS5yEBZko5eq7uhXStEbnyQ== +matrix-widget-api@^0.1.0-beta.14: + version "0.1.0-beta.14" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70" + integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ== dependencies: "@types/events" "^3.0.0" events "^3.2.0" @@ -6151,10 +6148,6 @@ object.values@^1.1.1, object.values@^1.1.2: es-abstract "^1.18.0-next.1" has "^1.0.3" -"olm@https://packages.matrix.org/npm/olm/olm-3.2.1.tgz": - version "3.2.1" - resolved "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz#d623d76f99c3518dde68be8c86618d68bc7b004a" - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -6307,7 +6300,12 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5-htmlparser2-tree-adapter@^6.0.0: +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= + +parse5-htmlparser2-tree-adapter@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== @@ -6319,7 +6317,7 @@ parse5@5.1.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== -parse5@^6.0.0, parse5@^6.0.1: +parse5@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== @@ -7248,17 +7246,18 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -"sanitize-html@github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db": - version "2.0.0-rc.3" - resolved "https://codeload.github.com/apostrophecms/sanitize-html/tar.gz/3c7f93f2058f696f5359e3e58d464161647226db" +sanitize-html@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.3.3.tgz#3db382c9a621cce4c46d90f10c64f1e9da9e8353" + integrity sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" - htmlparser2 "^4.1.0" + htmlparser2 "^6.0.0" is-plain-object "^5.0.0" klona "^2.0.3" + parse-srcset "^1.0.2" postcss "^8.0.2" - srcset "^3.0.0" saxes@^5.0.0: version "5.0.1" @@ -7515,11 +7514,6 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -srcset@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/srcset/-/srcset-3.0.0.tgz#8afd8b971362dfc129ae9c1a99b3897301ce6441" - integrity sha512-D59vF08Qzu/C4GAOXVgMTLfgryt5fyWo93FZyhEWANo0PokFz/iWdDe13mX3O5TRf6l8vMTqckAfR4zPiaH0yQ== - sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -8004,6 +7998,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== +tslib@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" + integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== + tsutils@^3.17.1: version "3.19.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.19.1.tgz#d8566e0c51c82f32f9c25a4d367cd62409a547a9" @@ -8439,9 +8438,9 @@ write@1.0.3: mkdirp "^0.5.1" ws@^7.2.3: - version "7.4.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd" - integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA== + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== xml-name-validator@^3.0.0: version "3.0.0"