diff --git a/.eslintrc.js b/.eslintrc.js
index 429aa24993..c6aeb0d1be 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -29,10 +29,16 @@ module.exports = {
// so we replace it with a version that is class property aware
"babel/no-invalid-this": "error",
+ // We appear to follow this most of the time, so let's enforce it instead
+ // of occasionally following it (or catching it in review)
+ "keyword-spacing": "error",
+
/** react **/
// This just uses the react plugin to help eslint known when
// variables have been used in JSX
"react/jsx-uses-vars": "error",
+ // Don't mark React as unused if we're using JSX
+ "react/jsx-uses-react": "error",
// bind or arrow function in props causes performance issues
"react/jsx-no-bind": ["error", {
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c2af18e43..87459882c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,287 @@
+Changes in [0.11.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.3) (2017-12-04)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.2...v0.11.3)
+
+ * Bump js-sdk version to pull in fix for [setting room publicity in a group](https://github.com/matrix-org/matrix-js-sdk/commit/aa3201ebb0fff5af2fb733080aa65ed1f7213de6).
+
+Changes in [0.11.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.2) (2017-11-28)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.1...v0.11.2)
+
+ * Ignore unrecognised login flows
+ [\#1633](https://github.com/matrix-org/matrix-react-sdk/pull/1633)
+
+Changes in [0.11.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.1) (2017-11-17)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0...v0.11.1)
+
+ * Fix the force TURN option
+ [\#1621](https://github.com/matrix-org/matrix-react-sdk/pull/1621)
+
+Changes in [0.11.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0) (2017-11-15)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.3...v0.11.0)
+
+
+Changes in [0.11.0-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.3) (2017-11-14)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.2...v0.11.0-rc.3)
+
+
+Changes in [0.11.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.2) (2017-11-10)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.1...v0.11.0-rc.2)
+
+ * Make groups a fully-fleged baked-in feature
+ [\#1603](https://github.com/matrix-org/matrix-react-sdk/pull/1603)
+
+Changes in [0.11.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.1) (2017-11-10)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7...v0.11.0-rc.1)
+
+ * Improve widget rendering on prop updates
+ [\#1548](https://github.com/matrix-org/matrix-react-sdk/pull/1548)
+ * Display group member profile (avatar/displayname) in ConfirmUserActionDialog
+ [\#1595](https://github.com/matrix-org/matrix-react-sdk/pull/1595)
+ * Don't crash if there isn't a room notif rule
+ [\#1602](https://github.com/matrix-org/matrix-react-sdk/pull/1602)
+ * Show group name in flair tooltip if one is set
+ [\#1596](https://github.com/matrix-org/matrix-react-sdk/pull/1596)
+ * Convert group avatar URL to HTTP before handing to BaseAvatar
+ [\#1597](https://github.com/matrix-org/matrix-react-sdk/pull/1597)
+ * Add group features as starting points for ILAG
+ [\#1601](https://github.com/matrix-org/matrix-react-sdk/pull/1601)
+ * Modify the group room visibility API to reflect the js-sdk changes
+ [\#1598](https://github.com/matrix-org/matrix-react-sdk/pull/1598)
+ * Update from Weblate.
+ [\#1599](https://github.com/matrix-org/matrix-react-sdk/pull/1599)
+ * Revert "UnknownDeviceDialog: get devices from SDK"
+ [\#1594](https://github.com/matrix-org/matrix-react-sdk/pull/1594)
+ * Order users in the group member list with admins first
+ [\#1591](https://github.com/matrix-org/matrix-react-sdk/pull/1591)
+ * Fetch group members after accepting an invite
+ [\#1592](https://github.com/matrix-org/matrix-react-sdk/pull/1592)
+ * Improve address picker for rooms
+ [\#1589](https://github.com/matrix-org/matrix-react-sdk/pull/1589)
+ * Fix FlairStore getPublicisedGroupsCached to give the correct, existing
+ promise
+ [\#1590](https://github.com/matrix-org/matrix-react-sdk/pull/1590)
+ * Use the getProfileInfo API for group inviter profile
+ [\#1585](https://github.com/matrix-org/matrix-react-sdk/pull/1585)
+ * Add checkbox to GroupAddressPicker for determining visibility of group rooms
+ [\#1587](https://github.com/matrix-org/matrix-react-sdk/pull/1587)
+ * Alter group member api
+ [\#1581](https://github.com/matrix-org/matrix-react-sdk/pull/1581)
+ * Improve group creation UX
+ [\#1580](https://github.com/matrix-org/matrix-react-sdk/pull/1580)
+ * Disable RoomDetailList in GroupView when editing
+ [\#1583](https://github.com/matrix-org/matrix-react-sdk/pull/1583)
+ * Default to no read pins if there is no applicable account data
+ [\#1586](https://github.com/matrix-org/matrix-react-sdk/pull/1586)
+ * UnknownDeviceDialog: get devices from SDK
+ [\#1584](https://github.com/matrix-org/matrix-react-sdk/pull/1584)
+ * Add a small indicator for when a new event is pinned
+ [\#1486](https://github.com/matrix-org/matrix-react-sdk/pull/1486)
+ * Implement tooltip for group rooms
+ [\#1582](https://github.com/matrix-org/matrix-react-sdk/pull/1582)
+ * Room notifs in autocomplete & composer
+ [\#1577](https://github.com/matrix-org/matrix-react-sdk/pull/1577)
+ * Ignore img tags in HTML if src is not specified
+ [\#1579](https://github.com/matrix-org/matrix-react-sdk/pull/1579)
+ * Indicate admins in the group member list with a sheriff badge
+ [\#1578](https://github.com/matrix-org/matrix-react-sdk/pull/1578)
+ * Remember whether widget drawer was hidden per-room
+ [\#1533](https://github.com/matrix-org/matrix-react-sdk/pull/1533)
+ * Throw an error when trying to create a group store with falsey groupId
+ [\#1576](https://github.com/matrix-org/matrix-react-sdk/pull/1576)
+ * Fixes React warning
+ [\#1571](https://github.com/matrix-org/matrix-react-sdk/pull/1571)
+ * Fix Flair not appearing due to missing this._usersInFlight
+ [\#1575](https://github.com/matrix-org/matrix-react-sdk/pull/1575)
+ * Use, if possible, a room's canonical or first alias when viewing the …
+ [\#1574](https://github.com/matrix-org/matrix-react-sdk/pull/1574)
+ * Add CSS classes to group ID input in CreateGroupDialog
+ [\#1573](https://github.com/matrix-org/matrix-react-sdk/pull/1573)
+ * Give autocomplete providers the room they're in
+ [\#1568](https://github.com/matrix-org/matrix-react-sdk/pull/1568)
+ * Fix multiple pills on one line
+ [\#1572](https://github.com/matrix-org/matrix-react-sdk/pull/1572)
+ * Fix group invites such that they look similar to room invites
+ [\#1570](https://github.com/matrix-org/matrix-react-sdk/pull/1570)
+ * Add a GeminiScrollbar to Your Communities
+ [\#1569](https://github.com/matrix-org/matrix-react-sdk/pull/1569)
+ * Fix multiple requests for publicised groups of given user
+ [\#1567](https://github.com/matrix-org/matrix-react-sdk/pull/1567)
+ * Add toggle to alter visibility of a room-group association
+ [\#1566](https://github.com/matrix-org/matrix-react-sdk/pull/1566)
+ * Pillify room notifs in the timeline
+ [\#1564](https://github.com/matrix-org/matrix-react-sdk/pull/1564)
+ * Implement simple GroupRoomInfo
+ [\#1563](https://github.com/matrix-org/matrix-react-sdk/pull/1563)
+ * turn NPE on flair resolution errors into a logged error
+ [\#1565](https://github.com/matrix-org/matrix-react-sdk/pull/1565)
+ * Less translation in parts
+ [\#1484](https://github.com/matrix-org/matrix-react-sdk/pull/1484)
+ * Redact group IDs from analytics
+ [\#1562](https://github.com/matrix-org/matrix-react-sdk/pull/1562)
+ * Display whether the group summary/room list is loading
+ [\#1560](https://github.com/matrix-org/matrix-react-sdk/pull/1560)
+ * Change client-side validation of group IDs to match synapse
+ [\#1558](https://github.com/matrix-org/matrix-react-sdk/pull/1558)
+ * Prevent non-members from opening group settings
+ [\#1559](https://github.com/matrix-org/matrix-react-sdk/pull/1559)
+ * Alter UI for disinviting a group member
+ [\#1556](https://github.com/matrix-org/matrix-react-sdk/pull/1556)
+ * Only show admin tools to privileged users
+ [\#1555](https://github.com/matrix-org/matrix-react-sdk/pull/1555)
+ * Try lowercase username on login
+ [\#1550](https://github.com/matrix-org/matrix-react-sdk/pull/1550)
+ * Don't refresh page on password change prompt
+ [\#1554](https://github.com/matrix-org/matrix-react-sdk/pull/1554)
+ * Fix initial in GroupAvatar in GroupView
+ [\#1553](https://github.com/matrix-org/matrix-react-sdk/pull/1553)
+ * Use "crop" method to scale group avatars in MyGroups
+ [\#1549](https://github.com/matrix-org/matrix-react-sdk/pull/1549)
+ * Lowercase all usernames
+ [\#1547](https://github.com/matrix-org/matrix-react-sdk/pull/1547)
+ * Add sensible missing entry generator for MELS tests
+ [\#1546](https://github.com/matrix-org/matrix-react-sdk/pull/1546)
+ * Fix prompt to re-use chat room
+ [\#1545](https://github.com/matrix-org/matrix-react-sdk/pull/1545)
+ * Add unregiseterListener to GroupStore
+ [\#1544](https://github.com/matrix-org/matrix-react-sdk/pull/1544)
+ * Fix groups invited users err for non members
+ [\#1543](https://github.com/matrix-org/matrix-react-sdk/pull/1543)
+ * Add Mention button to MemberInfo
+ [\#1532](https://github.com/matrix-org/matrix-react-sdk/pull/1532)
+ * Only show group settings cog to members
+ [\#1541](https://github.com/matrix-org/matrix-react-sdk/pull/1541)
+ * Use correct icon for group room deletion and make themeable
+ [\#1540](https://github.com/matrix-org/matrix-react-sdk/pull/1540)
+ * Add invite button to MemberInfo if user has left or wasn't in room
+ [\#1534](https://github.com/matrix-org/matrix-react-sdk/pull/1534)
+ * Add option to mirror local video feed
+ [\#1539](https://github.com/matrix-org/matrix-react-sdk/pull/1539)
+ * Use the correct userId when displaying who redacted a message
+ [\#1538](https://github.com/matrix-org/matrix-react-sdk/pull/1538)
+ * Only show editing UI for aliases/related_groups for users /w power
+ [\#1529](https://github.com/matrix-org/matrix-react-sdk/pull/1529)
+ * Swap from `ui_opacity` to `panel_disabled`
+ [\#1535](https://github.com/matrix-org/matrix-react-sdk/pull/1535)
+ * Fix room address picker tiles default name
+ [\#1536](https://github.com/matrix-org/matrix-react-sdk/pull/1536)
+ * T3chguy/hide level change on 50
+ [\#1531](https://github.com/matrix-org/matrix-react-sdk/pull/1531)
+ * fix missing date sep caused by hidden event at start of day
+ [\#1537](https://github.com/matrix-org/matrix-react-sdk/pull/1537)
+ * Add a delete confirmation dialog for widgets
+ [\#1520](https://github.com/matrix-org/matrix-react-sdk/pull/1520)
+ * When dispatching view_[my_]group[s], reset RoomViewStore
+ [\#1530](https://github.com/matrix-org/matrix-react-sdk/pull/1530)
+ * Prevent editing of UI requiring user privilege if user unprivileged
+ [\#1528](https://github.com/matrix-org/matrix-react-sdk/pull/1528)
+ * Use the correct property of the API room objects
+ [\#1526](https://github.com/matrix-org/matrix-react-sdk/pull/1526)
+ * Don't include the |other in the translation value
+ [\#1527](https://github.com/matrix-org/matrix-react-sdk/pull/1527)
+ * Re-run gen-i18n after fixing https://github.com/matrix-org/matrix-react-
+ sdk/pull/1521
+ [\#1525](https://github.com/matrix-org/matrix-react-sdk/pull/1525)
+ * Fix some react warnings in GroupMemberList
+ [\#1522](https://github.com/matrix-org/matrix-react-sdk/pull/1522)
+ * Fix bug with gen-i18n/js when adding new plurals
+ [\#1521](https://github.com/matrix-org/matrix-react-sdk/pull/1521)
+ * Make GroupStoreCache global for cross-package access
+ [\#1524](https://github.com/matrix-org/matrix-react-sdk/pull/1524)
+ * Add fields needed by RoomDetailList to groupRoomFromApiObject
+ [\#1523](https://github.com/matrix-org/matrix-react-sdk/pull/1523)
+ * Only show flair for groups with avatars set
+ [\#1519](https://github.com/matrix-org/matrix-react-sdk/pull/1519)
+ * Refresh group member lists after inviting users
+ [\#1518](https://github.com/matrix-org/matrix-react-sdk/pull/1518)
+ * Invalidate the user's public groups cache when changing group publicity
+ [\#1517](https://github.com/matrix-org/matrix-react-sdk/pull/1517)
+ * Make the gen-i18n script validate _t calls
+ [\#1515](https://github.com/matrix-org/matrix-react-sdk/pull/1515)
+ * Add placeholder to MyGroups page, adjust CSS classes
+ [\#1514](https://github.com/matrix-org/matrix-react-sdk/pull/1514)
+ * Rxl881/parallelshell
+ [\#1338](https://github.com/matrix-org/matrix-react-sdk/pull/1338)
+ * Run prunei18n
+ [\#1513](https://github.com/matrix-org/matrix-react-sdk/pull/1513)
+ * Update from Weblate.
+ [\#1512](https://github.com/matrix-org/matrix-react-sdk/pull/1512)
+ * Add script to prune unused translations
+ [\#1502](https://github.com/matrix-org/matrix-react-sdk/pull/1502)
+ * Fix creation of DM rooms
+ [\#1510](https://github.com/matrix-org/matrix-react-sdk/pull/1510)
+ * Group create dialog: only enter localpart
+ [\#1507](https://github.com/matrix-org/matrix-react-sdk/pull/1507)
+ * Improve MyGroups UI
+ [\#1509](https://github.com/matrix-org/matrix-react-sdk/pull/1509)
+ * Use object URLs to load Files in to images
+ [\#1508](https://github.com/matrix-org/matrix-react-sdk/pull/1508)
+ * Add clientside error for non-alphanumeric group ID
+ [\#1506](https://github.com/matrix-org/matrix-react-sdk/pull/1506)
+ * Fix invites to groups without names
+ [\#1505](https://github.com/matrix-org/matrix-react-sdk/pull/1505)
+ * Add warning when adding group rooms/users
+ [\#1504](https://github.com/matrix-org/matrix-react-sdk/pull/1504)
+ * More Groups->Communities
+ [\#1503](https://github.com/matrix-org/matrix-react-sdk/pull/1503)
+ * Groups -> Communities
+ [\#1501](https://github.com/matrix-org/matrix-react-sdk/pull/1501)
+ * Factor out Flair cache into FlairStore
+ [\#1500](https://github.com/matrix-org/matrix-react-sdk/pull/1500)
+ * Add i18n script to package.json
+ [\#1499](https://github.com/matrix-org/matrix-react-sdk/pull/1499)
+ * Make gen-i18n support 'HTML'
+ [\#1498](https://github.com/matrix-org/matrix-react-sdk/pull/1498)
+ * fix editing visuals on groupview header
+ [\#1497](https://github.com/matrix-org/matrix-react-sdk/pull/1497)
+ * Script to generate the translations base file
+ [\#1493](https://github.com/matrix-org/matrix-react-sdk/pull/1493)
+ * Update from Weblate.
+ [\#1495](https://github.com/matrix-org/matrix-react-sdk/pull/1495)
+ * Attempt to relate a group to a room when adding it
+ [\#1494](https://github.com/matrix-org/matrix-react-sdk/pull/1494)
+ * Shuffle GroupView UI
+ [\#1490](https://github.com/matrix-org/matrix-react-sdk/pull/1490)
+ * Fix bug preventing partial group profile
+ [\#1491](https://github.com/matrix-org/matrix-react-sdk/pull/1491)
+ * Don't show room IDs when picking rooms
+ [\#1492](https://github.com/matrix-org/matrix-react-sdk/pull/1492)
+ * Only show invited section if there are invited group members
+ [\#1489](https://github.com/matrix-org/matrix-react-sdk/pull/1489)
+ * Show "Invited" section in the user list
+ [\#1488](https://github.com/matrix-org/matrix-react-sdk/pull/1488)
+ * Refactor class names for an entity tile being hovered over
+ [\#1487](https://github.com/matrix-org/matrix-react-sdk/pull/1487)
+ * Modify GroupView UI
+ [\#1475](https://github.com/matrix-org/matrix-react-sdk/pull/1475)
+ * Message/event pinning
+ [\#1439](https://github.com/matrix-org/matrix-react-sdk/pull/1439)
+ * Remove duplicate declaration that breaks the build
+ [\#1483](https://github.com/matrix-org/matrix-react-sdk/pull/1483)
+ * Include magnet scheme in sanitize HTML params
+ [\#1301](https://github.com/matrix-org/matrix-react-sdk/pull/1301)
+ * Add a way to jump to a user's Read Receipt from MemberInfo
+ [\#1454](https://github.com/matrix-org/matrix-react-sdk/pull/1454)
+ * Use standard subsitution syntax in _tJsx
+ [\#1462](https://github.com/matrix-org/matrix-react-sdk/pull/1462)
+ * Don't suggest grey as a color scheme for a room
+ [\#1442](https://github.com/matrix-org/matrix-react-sdk/pull/1442)
+ * allow hiding of notification body for privacy reasons
+ [\#1362](https://github.com/matrix-org/matrix-react-sdk/pull/1362)
+ * Suggest to invite people when speaking in an empty room
+ [\#1466](https://github.com/matrix-org/matrix-react-sdk/pull/1466)
+ * Buttons to remove room/self avatar
+ [\#1478](https://github.com/matrix-org/matrix-react-sdk/pull/1478)
+ * T3chguy/fix memberlist
+ [\#1480](https://github.com/matrix-org/matrix-react-sdk/pull/1480)
+ * add option to disable BigEmoji
+ [\#1481](https://github.com/matrix-org/matrix-react-sdk/pull/1481)
+
Changes in [0.10.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7) (2017-10-16)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.3...v0.10.7)
diff --git a/docs/settings.md b/docs/settings.md
new file mode 100644
index 0000000000..d41aebad3c
--- /dev/null
+++ b/docs/settings.md
@@ -0,0 +1,151 @@
+# Settings Reference
+
+This document serves as developer documentation for using "Granular Settings". Granular Settings allow users to specify different values for a setting at particular levels of interest. For example, a user may say that in a particular room they want URL previews off, but in all other rooms they want them enabled. The `SettingsStore` helps mask the complexity of dealing with the different levels and exposes easy to use getters and setters.
+
+
+## Levels
+
+Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in order of prioirty, are:
+* `device` - The current user's device
+* `room-device` - The current user's device, but only when in a specific room
+* `room-account` - The current user's account, but only when in a specific room
+* `account` - The current user's account
+* `room` - A specific room (setting for all members of the room)
+* `config` - Values are defined by `config.json`
+* `default` - The hardcoded default for the settings
+
+Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure that room administrators cannot force account-only settings upon participants.
+
+
+## Settings
+
+Settings are the different options a user may set or experience in the application. These are pre-defined in `src/settings/Settings.js` under the `SETTINGS` constant and have the following minimum requirements:
+```
+// The ID is used to reference the setting throughout the application. This must be unique.
+"theSettingId": {
+ // The levels this setting supports is required. In `src/settings/Settings.js` there are various pre-set arrays
+ // for this option - they should be used where possible to avoid copy/pasting arrays across settings.
+ supportedLevels: [...],
+
+ // The default for this setting serves two purposes: It provides a value if the setting is not defined at other
+ // levels, and it serves to demonstrate the expected type to other developers. The value isn't enforced, but it
+ // should be respected throughout the code. The default may be any data type.
+ default: false,
+
+ // The display name has two notations: string and object. The object notation allows for different translatable
+ // strings to be used for different levels, while the string notation represents the string for all levels.
+
+ displayName: _td("Change something"), // effectively `displayName: { "default": _td("Change something") }`
+ displayName: {
+ "room": _td("Change something for participants of this room"),
+
+ // Note: the default will be used if the level requested (such as `device`) does not have a string defined here.
+ "default": _td("Change something"),
+ }
+}
+```
+
+### Getting values for a setting
+
+After importing `SettingsStore`, simply make a call to `SettingsStore.getValue`. The `roomId` parameter should always be supplied where possible, even if the setting does not have a per-room level value. This is to ensure that the value returned is best represented in the room, particularly if the setting ever gets a per-room level in the future.
+
+In settings pages it is often desired to have the value at a particular level instead of getting the calculated value. Call `SettingsStore.getValueAt` to get the value of a setting at a particular level, and optionally make it explicitly at that level. By default `getValueAt` will traverse the tree starting at the provided level; making it explicit means it will not go beyond the provided level. When using `getValueAt`, please be sure to use `SettingLevel` to represent the target level.
+
+### Setting values for a setting
+
+Values are defined at particular levels and should be done in a safe manner. There are two checks to perform to ensure a clean save: is the level supported and can the user actually set the value. In most cases, neither should be an issue although there are circumstances where this changes. An example of a safe call is:
+```javascript
+const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM);
+if (isSupported) {
+ const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM);
+ if (canSetValue) {
+ SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue);
+ }
+}
+```
+
+These checks may also be performed in different areas of the application to avoid the verbose example above. For instance, the component which allows changing the setting may be hidden conditionally on the above conditions.
+
+##### `SettingsFlag` component
+
+Where possible, the `SettingsFlag` component should be used to set simple "flip-a-bit" (true/false) settings. The `SettingsFlag` also supports simple radio button options, such as the theme the user would like to use.
+```html
+
+```
+
+### Getting the display name for a setting
+
+Simply call `SettingsStore.getDisplayName`. The appropriate display name will be returned and automatically translated for you. If a display name cannot be found, it will return `null`.
+
+
+## Features
+
+Occasionally some parts of the application may be undergoing testing and are not quite production ready. These are commonly known to be behind a "labs flag". Features behind lab flags must go through the granular settings system, and look and act very much normal settings. The exception is that they must supply `isFeature: true` as part of the setting definition and should go through the helper functions on `SettingsStore`.
+
+### Determining if a feature is enabled
+
+A simple call to `SettingsStore.isFeatureEnabled` will tell you if the feature is enabled. This will perform all the required calculations to determine if the feature is enabled based upon the configuration and user selection.
+
+### Enabling a feature
+
+Features can only be enabled if the feature is in the `labs` state, otherwise this is a no-op. To find the current set of features in the `labs` state, call `SettingsStore.getLabsFeatures`. To set the value, call `SettingsStore.setFeatureEnabled`.
+
+
+## Setting controllers
+
+Settings may have environmental factors that affect their value or need additional code to be called when they are modified. A setting controller is able to override the calculated value for a setting and react to changes in that setting. Controllers are not a replacement for the level handlers and should only be used to ensure the environment is kept up to date with the setting where it is otherwise not possible. An example of this is the notification settings: they can only be considered enabled if the platform supports notifications, and enabling notifications requires additional steps to actually enable notifications.
+
+For more information, see `src/settings/controllers/SettingController.js`.
+
+
+## Local echo
+
+`SettingsStore` will perform local echo on all settings to ensure that immediately getting values does not cause a split-brain scenario. As mentioned in the "Setting values for a setting" section, the appropriate checks should be done to ensure that the user is allowed to set the value. The local echo system assumes that the user has permission and that the request will go through successfully. The local echo only takes effect until the request to save a setting has completed (either successfully or otherwise).
+
+```javascript
+SettingsStore.setValue(...).then(() => {
+ // The value has actually been stored at this point.
+});
+SettingsStore.getValue(...); // this will return the value set in `setValue` above.
+```
+
+
+
+# Maintainers Reference
+
+The granular settings system has a few complex parts to power it. This section is to document how the `SettingsStore` is supposed to work.
+
+### General information
+
+The `SettingsStore` uses the hardcoded `LEVEL_ORDER` constant to ensure that it is using the correct override procedure. The array is checked from left to right, simulating the behaviour of overriding values from the higher levels. Each level should be defined in this array, including `default`.
+
+Handlers (`src/settings/handlers/SettingsHandler.js`) represent a single level and are responsible for getting and setting values at that level. Handlers also provide additional information to the `SettingsStore` such as if the level is supported or if the current user may set values at the level. The `SettingsStore` will use the handler to enforce checks and manipulate settings. Handlers are also responsible for dealing with migration patterns or legacy settings for their level (for example, a setting being renamed or using a different key from other settings in the underlying store). Handlers are provided to the `SettingsStore` via the `LEVEL_HANDLERS` constant. `SettingsStore` will optimize lookups by only considering handlers that are supported on the platform.
+
+Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.js` which acts as a wrapper around a given handler. This is automatically applied to all defined `LEVEL_HANDLERS` and proxies the calls to the wrapped handler where possible. The echo is achieved by a simple object cache stored within the class itself. The cache is invalidated immediately upon the proxied save call succeeding or failing.
+
+Controllers are notified of changes by the `SettingsStore`, and are given the opportunity to override values after the `SettingsStore` has deemed the value calculated. Controllers are invoked as the last possible step in the code.
+
+### Features
+
+Features automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enable_labs` is false/not set. Features are always checked against the configuration before going through the level order as they have the option of being forced-on or forced-off for the application. This is done by the `features` section and looks something like this:
+
+```
+"features": {
+ "feature_groups": "enable",
+ "feature_pinning": "disable", // the default
+ "feature_presence": "labs"
+}
+```
+
+If `enableLabs` is true in the configuration, the default for features becomes `"labs"`.
diff --git a/package.json b/package.json
index 883fdae8d5..5c81db2153 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "0.10.7",
+ "version": "0.11.3",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -71,9 +71,10 @@
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3",
"lodash": "^4.13.1",
- "matrix-js-sdk": "0.8.5",
+ "matrix-js-sdk": "0.9.2",
"optimist": "^0.6.1",
"prop-types": "^15.5.8",
+ "querystring": "^0.2.0",
"react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2",
"react-dom": "^15.4.0",
diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js
index dd990b5210..fa9ccc8ed7 100755
--- a/scripts/gen-i18n.js
+++ b/scripts/gen-i18n.js
@@ -32,7 +32,7 @@ const walk = require('walk');
const flowParser = require('flow-parser');
const estreeWalker = require('estree-walker');
-const TRANSLATIONS_FUNCS = ['_t', '_td', '_tJsx'];
+const TRANSLATIONS_FUNCS = ['_t', '_td'];
const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json';
const OUTPUT_FILE = 'src/i18n/strings/en_EN.json';
@@ -126,7 +126,7 @@ function getTranslationsJs(file) {
if (tKey === null) return;
// check the format string against the args
- // We only check _t: _tJsx is much more complex and _td has no args
+ // We only check _t: _td has no args
if (node.callee.name === '_t') {
try {
const placeholders = getFormatStrings(tKey);
@@ -139,6 +139,22 @@ function getTranslationsJs(file) {
throw new Error(`No value found for placeholder '${placeholder}'`);
}
}
+
+ // Validate tag replacements
+ if (node.arguments.length > 2) {
+ const tagMap = node.arguments[2];
+ for (const prop of tagMap.properties) {
+ if (prop.key.type === 'Literal') {
+ const tag = prop.key.value;
+ // RegExp same as in src/languageHandler.js
+ const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`);
+ if (!tKey.match(regexp)) {
+ throw new Error(`No match for ${regexp} in ${tKey}`);
+ }
+ }
+ }
+ }
+
} catch (e) {
console.log();
console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`);
diff --git a/src/CallHandler.js b/src/CallHandler.js
index a9539d40e1..dd9d93709f 100644
--- a/src/CallHandler.js
+++ b/src/CallHandler.js
@@ -52,7 +52,6 @@ limitations under the License.
*/
import MatrixClientPeg from './MatrixClientPeg';
-import UserSettingsStore from './UserSettingsStore';
import PlatformPeg from './PlatformPeg';
import Modal from './Modal';
import sdk from './index';
@@ -245,9 +244,7 @@ function _onAction(payload) {
return;
} else if (members.length === 2) {
console.log("Place %s call in %s", payload.type, payload.room_id);
- const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
- forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
- });
+ const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
placeCall(call);
} else { // > 2
dis.dispatch({
diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js
index 839b496845..cdc5c61921 100644
--- a/src/CallMediaHandler.js
+++ b/src/CallMediaHandler.js
@@ -14,8 +14,8 @@
limitations under the License.
*/
-import UserSettingsStore from './UserSettingsStore';
import * as Matrix from 'matrix-js-sdk';
+import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
export default {
getDevices: function() {
@@ -43,22 +43,20 @@ export default {
},
loadDevices: function() {
- // this.getDevices().then((devices) => {
- const localSettings = UserSettingsStore.getLocalSettings();
- // // if deviceId is not found, automatic fallback is in spec
- // // recall previously stored inputs if any
- Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']);
- Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']);
- // });
+ const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
+ const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
+
+ Matrix.setMatrixCallAudioInput(audioDeviceId);
+ Matrix.setMatrixCallVideoInput(videoDeviceId);
},
setAudioInput: function(deviceId) {
- UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId);
+ SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallAudioInput(deviceId);
},
setVideoInput: function(deviceId) {
- UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId);
+ SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallVideoInput(deviceId);
},
};
diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js
index 2fff3882b4..2757c5bd3d 100644
--- a/src/ComposerHistoryManager.js
+++ b/src/ComposerHistoryManager.js
@@ -61,7 +61,7 @@ export default class ComposerHistoryManager {
// TODO: Performance issues?
let item;
- for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
+ for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
this.history.push(
Object.assign(new HistoryItem(), JSON.parse(item)),
);
diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js
index 595f0cfe46..ef9010cbf2 100644
--- a/src/GroupAddressPicker.js
+++ b/src/GroupAddressPicker.js
@@ -49,20 +49,26 @@ export function showGroupInviteDialog(groupId) {
export function showGroupAddRoomDialog(groupId) {
return new Promise((resolve, reject) => {
+ let addRoomsPublicly = false;
+ const onCheckboxClicked = (e) => {
+ addRoomsPublicly = e.target.checked;
+ };
const description =
{ _t("Which rooms would you like to add to this community?") }
-
- { _t(
- "Warning: any room you add to a community will be publicly "+
- "visible to anyone who knows the community ID",
- ) }
-
;
+ const checkboxContainer = ;
+
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
title: _t("Add rooms to the community"),
description: description,
+ extraNode: checkboxContainer,
placeholder: _t("Room name or alias"),
button: _t("Add to community"),
pickerType: 'room',
@@ -70,7 +76,7 @@ export function showGroupAddRoomDialog(groupId) {
onFinished: (success, addrs) => {
if (!success) return;
- _onGroupAddRoomFinished(groupId, addrs).then(resolve, reject);
+ _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly).then(resolve, reject);
},
});
});
@@ -106,13 +112,13 @@ function _onGroupInviteFinished(groupId, addrs) {
});
}
-function _onGroupAddRoomFinished(groupId, addrs) {
+function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
const matrixClient = MatrixClientPeg.get();
- const groupStore = GroupStoreCache.getGroupStore(matrixClient, groupId);
+ const groupStore = GroupStoreCache.getGroupStore(groupId);
const errorList = [];
return Promise.all(addrs.map((addr) => {
return groupStore
- .addRoomToGroup(addr.address)
+ .addRoomToGroup(addr.address, addRoomsPublicly)
.catch(() => { errorList.push(addr.address); })
.then(() => {
const roomId = addr.address;
diff --git a/src/KeyCode.js b/src/Keyboard.js
similarity index 77%
rename from src/KeyCode.js
rename to src/Keyboard.js
index ec5595b71b..9c872e1c66 100644
--- a/src/KeyCode.js
+++ b/src/Keyboard.js
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
+Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,7 +16,7 @@ limitations under the License.
*/
/* a selection of key codes, as used in KeyboardEvent.keyCode */
-module.exports = {
+export const KeyCode = {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
@@ -58,3 +59,12 @@ module.exports = {
KEY_Y: 89,
KEY_Z: 90,
};
+
+export function isOnlyCtrlOrCmdKeyEvent(ev) {
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
+ if (isMac) {
+ return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
+ } else {
+ return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
+ }
+}
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index 4d8911f7a6..efd5c20d5c 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -389,6 +389,8 @@ function _persistCredentialsToLocalStorage(credentials) {
* Logs the current session out and transitions to the logged-out state
*/
export function logout() {
+ if (!MatrixClientPeg.get()) return;
+
if (MatrixClientPeg.get().isGuest()) {
// logout doesn't work for guest sessions
// Also we sometimes want to re-log in a guest session
@@ -436,6 +438,10 @@ function startMatrixClient() {
DMRoomMap.makeShared().start();
MatrixClientPeg.start();
+
+ // dispatch that we finished starting up to wire up any other bits
+ // of the matrix client that cannot be set prior to starting up.
+ dis.dispatch({action: 'client_started'});
}
/*
diff --git a/src/Login.js b/src/Login.js
index 55e996ce80..61a14959d8 100644
--- a/src/Login.js
+++ b/src/Login.js
@@ -204,6 +204,12 @@ export default class Login {
}
throw originalLoginError;
}).catch((error) => {
+ // We apparently squash case at login serverside these days:
+ // https://github.com/matrix-org/synapse/blob/1189be43a2479f5adf034613e8d10e3f4f452eb9/synapse/handlers/auth.py#L475
+ // so this wasn't needed after all. Keeping the code around in case the
+ // the situation changes...
+
+ /*
if (
error.httpStatus === 403 &&
loginParams.identifier.type === 'm.id.user' &&
@@ -211,6 +217,7 @@ export default class Login {
) {
return tryLowercaseUsername(originalLoginError);
}
+ */
throw originalLoginError;
}).catch((error) => {
console.log("Login failed", error);
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index 0c3d5b3775..a6012f5213 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -21,6 +21,7 @@ import utils from 'matrix-js-sdk/lib/utils';
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
import createMatrixClient from './utils/createMatrixClient';
+import SettingsStore from './settings/SettingsStore';
interface MatrixClientCreds {
homeserverUrl: string,
@@ -84,7 +85,7 @@ class MatrixClientPeg {
if (this.matrixClient.initCrypto) {
await this.matrixClient.initCrypto();
}
- } catch(e) {
+ } catch (e) {
// this can happen for a number of reasons, the most likely being
// that the olm library was missing. It's not fatal.
console.warn("Unable to initialise e2e: " + e);
@@ -93,12 +94,13 @@ class MatrixClientPeg {
const opts = utils.deepCopy(this.opts);
// the react sdk doesn't work without this, so don't allow
opts.pendingEventOrdering = "detached";
+ opts.disablePresence = true; // we do this manually
try {
const promise = this.matrixClient.store.startup();
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
await promise;
- } catch(err) {
+ } catch (err) {
// log any errors when starting up the database (if one exists)
console.error(`Error starting matrixclient store: ${err}`);
}
@@ -143,6 +145,7 @@ class MatrixClientPeg {
userId: creds.userId,
deviceId: creds.deviceId,
timelineSupport: true,
+ forceTURN: SettingsStore.getValue('webRtcForceTURN', false),
};
this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript);
diff --git a/src/Notifier.js b/src/Notifier.js
index 93ef192fe0..75b698862c 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -25,6 +25,7 @@ import dis from './dispatcher';
import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
+import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
/*
* Dispatches:
@@ -138,10 +139,8 @@ const Notifier = {
// make sure that we persist the current setting audio_enabled setting
// before changing anything
- if (global.localStorage) {
- if (global.localStorage.getItem('audio_notifications_enabled') === null) {
- this.setAudioEnabled(this.isEnabled());
- }
+ if (SettingsStore.isLevelSupported(SettingLevel.DEVICE)) {
+ SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, this.isEnabled());
}
if (enable) {
@@ -149,6 +148,7 @@ const Notifier = {
plaf.requestNotificationPermission().done((result) => {
if (result !== 'granted') {
// The permission request was dismissed or denied
+ // TODO: Support alternative branding in messaging
const description = result === 'denied'
? _t('Riot does not have permission to send you notifications - please check your browser settings')
: _t('Riot was not given permission to send notifications - please try again');
@@ -160,10 +160,6 @@ const Notifier = {
return;
}
- if (global.localStorage) {
- global.localStorage.setItem('notifications_enabled', 'true');
- }
-
if (callback) callback();
dis.dispatch({
action: "notifier_enabled",
@@ -174,8 +170,6 @@ const Notifier = {
// disabled again in the future, we will show the banner again.
this.setToolbarHidden(false);
} else {
- if (!global.localStorage) return;
- global.localStorage.setItem('notifications_enabled', 'false');
dis.dispatch({
action: "notifier_enabled",
value: false,
@@ -184,44 +178,24 @@ const Notifier = {
},
isEnabled: function() {
+ return this.isPossible() && SettingsStore.getValue("notificationsEnabled");
+ },
+
+ isPossible: function() {
const plaf = PlatformPeg.get();
if (!plaf) return false;
if (!plaf.supportsNotifications()) return false;
if (!plaf.maySendNotifications()) return false;
- if (!global.localStorage) return true;
-
- const enabled = global.localStorage.getItem('notifications_enabled');
- if (enabled === null) return true;
- return enabled === 'true';
- },
-
- setBodyEnabled: function(enable) {
- if (!global.localStorage) return;
- global.localStorage.setItem('notifications_body_enabled', enable ? 'true' : 'false');
+ return true; // possible, but not necessarily enabled
},
isBodyEnabled: function() {
- if (!global.localStorage) return true;
- const enabled = global.localStorage.getItem('notifications_body_enabled');
- // default to true if the popups are enabled
- if (enabled === null) return this.isEnabled();
- return enabled === 'true';
+ return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled");
},
- setAudioEnabled: function(enable) {
- if (!global.localStorage) return;
- global.localStorage.setItem('audio_notifications_enabled',
- enable ? 'true' : 'false');
- },
-
- isAudioEnabled: function(enable) {
- if (!global.localStorage) return true;
- const enabled = global.localStorage.getItem(
- 'audio_notifications_enabled');
- // default to true if the popups are enabled
- if (enabled === null) return this.isEnabled();
- return enabled === 'true';
+ isAudioEnabled: function() {
+ return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled");
},
setToolbarHidden: function(hidden, persistent = true) {
@@ -238,16 +212,14 @@ const Notifier = {
// update the info to localStorage for persistent settings
if (persistent && global.localStorage) {
- global.localStorage.setItem('notifications_hidden', hidden);
+ global.localStorage.setItem("notifications_hidden", hidden);
}
},
isToolbarHidden: function() {
// Check localStorage for any such meta data
if (global.localStorage) {
- if (global.localStorage.getItem('notifications_hidden') === 'true') {
- return true;
- }
+ return global.localStorage.getItem("notifications_hidden") === "true";
}
return this.toolbarHidden;
diff --git a/src/Presence.js b/src/Presence.js
index fab518e1cb..2652c64c96 100644
--- a/src/Presence.js
+++ b/src/Presence.js
@@ -56,13 +56,27 @@ class Presence {
return this.state;
}
+ /**
+ * Get the current status message.
+ * @returns {String} the status message, may be null
+ */
+ getStatusMessage() {
+ return this.statusMessage;
+ }
+
/**
* Set the presence state.
* If the state has changed, the Home Server will be notified.
* @param {string} newState the new presence state (see PRESENCE enum)
+ * @param {String} statusMessage an optional status message for the presence
+ * @param {boolean} maintain true to have this status maintained by this tracker
*/
- setState(newState) {
- if (newState === this.state) {
+ setState(newState, statusMessage=null, maintain=false) {
+ if (this.maintain) {
+ // Don't update presence if we're maintaining a particular status
+ return;
+ }
+ if (newState === this.state && statusMessage === this.statusMessage) {
return;
}
if (PRESENCE_STATES.indexOf(newState) === -1) {
@@ -72,21 +86,37 @@ class Presence {
return;
}
const old_state = this.state;
+ const old_message = this.statusMessage;
this.state = newState;
+ this.statusMessage = statusMessage;
+ this.maintain = maintain;
if (MatrixClientPeg.get().isGuest()) {
return; // don't try to set presence when a guest; it won't work.
}
+ const updateContent = {
+ presence: this.state,
+ status_msg: this.statusMessage ? this.statusMessage : '',
+ };
+
const self = this;
- MatrixClientPeg.get().setPresence(this.state).done(function() {
+ MatrixClientPeg.get().setPresence(updateContent).done(function() {
console.log("Presence: %s", newState);
+
+ // We have to dispatch because the js-sdk is unreliable at telling us about our own presence
+ dis.dispatch({action: "self_presence_updated", statusInfo: updateContent});
}, function(err) {
console.error("Failed to set presence: %s", err);
self.state = old_state;
+ self.statusMessage = old_message;
});
}
+ stopMaintainingStatus() {
+ this.maintain = false;
+ }
+
/**
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
* @private
@@ -95,7 +125,8 @@ class Presence {
this.setState("unavailable");
}
- _onUserActivity() {
+ _onUserActivity(payload) {
+ if (payload.action === "sync_state" || payload.action === "self_presence_updated") return;
this._resetTimer();
}
diff --git a/src/RichText.js b/src/RichText.js
index b61ba0b9a4..12274ee9f3 100644
--- a/src/RichText.js
+++ b/src/RichText.js
@@ -68,7 +68,7 @@ function unicodeToEmojiUri(str) {
return unicodeChar;
} else {
// Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below
- if(unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') {
+ if (unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') {
unicodeChar = unicodeChar[0];
}
diff --git a/src/Roles.js b/src/Roles.js
index 83d8192c67..438b6c1236 100644
--- a/src/Roles.js
+++ b/src/Roles.js
@@ -15,19 +15,20 @@ limitations under the License.
*/
import { _t } from './languageHandler';
-export function levelRoleMap() {
+export function levelRoleMap(usersDefault) {
return {
undefined: _t('Default'),
- 0: _t('User'),
+ 0: _t('Restricted'),
+ [usersDefault]: _t('Default'),
50: _t('Moderator'),
100: _t('Admin'),
};
}
-export function textualPowerLevel(level, userDefault) {
- const LEVEL_ROLE_MAP = this.levelRoleMap();
+export function textualPowerLevel(level, usersDefault) {
+ const LEVEL_ROLE_MAP = this.levelRoleMap(usersDefault);
if (LEVEL_ROLE_MAP[level]) {
- return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`);
+ return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`);
} else {
return level;
}
diff --git a/src/SdkConfig.js b/src/SdkConfig.js
index 48ebf011f2..8df725a913 100644
--- a/src/SdkConfig.js
+++ b/src/SdkConfig.js
@@ -26,7 +26,7 @@ const DEFAULTS = {
class SdkConfig {
static get() {
- return global.mxReactSdkConfig;
+ return global.mxReactSdkConfig || {};
}
static put(cfg) {
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index 82665cc2f3..344bac1ddb 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -20,6 +20,7 @@ import Tinter from "./Tinter";
import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
+import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
class Command {
@@ -97,9 +98,7 @@ const commands = {
colorScheme.secondary_color = matches[4];
}
return success(
- MatrixClientPeg.get().setRoomAccountData(
- roomId, "org.matrix.room.color_scheme", colorScheme,
- ),
+ SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
);
}
}
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 51e3eb8dc9..1bdf5ad90c 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -151,9 +151,9 @@ function textForCallHangupEvent(event) {
const senderName = event.sender ? event.sender.name : _t('Someone');
const eventContent = event.getContent();
let reason = "";
- if(!MatrixClientPeg.get().supportsVoip()) {
+ if (!MatrixClientPeg.get().supportsVoip()) {
reason = _t('(not supported by this browser)');
- } else if(eventContent.reason) {
+ } else if (eventContent.reason) {
if (eventContent.reason === "ice_failed") {
reason = _t('(could not connect media)');
} else if (eventContent.reason === "invite_timeout") {
diff --git a/src/Tinter.js b/src/Tinter.js
index 6b23df8c9b..c7402c15be 100644
--- a/src/Tinter.js
+++ b/src/Tinter.js
@@ -1,5 +1,6 @@
/*
Copyright 2015 OpenMarket Ltd
+Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,148 +15,125 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-// FIXME: these vars should be bundled up and attached to
-// module.exports otherwise this will break when included by both
-// react-sdk and apps layered on top.
-
const DEBUG = 0;
-// The colour keys to be replaced as referred to in CSS
-const keyRgb = [
- "rgb(118, 207, 166)", // Vector Green
- "rgb(234, 245, 240)", // Vector Light Green
- "rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green)
-];
+// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
+function colorToRgb(color) {
+ if (!color) {
+ return [0, 0, 0];
+ }
-// Some algebra workings for calculating the tint % of Vector Green & Light Green
-// x * 118 + (1 - x) * 255 = 234
-// x * 118 + 255 - 255 * x = 234
-// x * 118 - x * 255 = 234 - 255
-// (255 - 118) x = 255 - 234
-// x = (255 - 234) / (255 - 118) = 0.16
-
-// The colour keys to be replaced as referred to in SVGs
-const keyHex = [
- "#76CFA6", // Vector Green
- "#EAF5F0", // Vector Light Green
- "#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green)
- "#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
-];
-
-// cache of our replacement colours
-// defaults to our keys.
-const colors = [
- keyHex[0],
- keyHex[1],
- keyHex[2],
- keyHex[3],
-];
-
-const cssFixups = [
- // {
- // style: a style object that should be fixed up taken from a stylesheet
- // attr: name of the attribute to be clobbered, e.g. 'color'
- // index: ordinal of primary, secondary or tertiary
- // }
-];
-
-// CSS attributes to be fixed up
-const cssAttrs = [
- "color",
- "backgroundColor",
- "borderColor",
- "borderTopColor",
- "borderBottomColor",
- "borderLeftColor",
-];
-
-const svgAttrs = [
- "fill",
- "stroke",
-];
-
-let cached = false;
-
-function calcCssFixups() {
- if (DEBUG) console.log("calcSvgFixups start");
- for (let i = 0; i < document.styleSheets.length; i++) {
- const ss = document.styleSheets[i];
- if (!ss) continue; // well done safari >:(
- // Chromium apparently sometimes returns null here; unsure why.
- // see $14534907369972FRXBx:matrix.org in HQ
- // ...ah, it's because there's a third party extension like
- // privacybadger inserting its own stylesheet in there with a
- // resource:// URI or something which results in a XSS error.
- // See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
- // ...except some browsers apparently return stylesheets without
- // hrefs, which we have no choice but ignore right now
-
- // XXX seriously? we are hardcoding the name of vector's CSS file in
- // here?
- //
- // Why do we need to limit it to vector's CSS file anyway - if there
- // are other CSS files affecting the doc don't we want to apply the
- // same transformations to them?
- //
- // Iterating through the CSS looking for matches to hack on feels
- // pretty horrible anyway. And what if the application skin doesn't use
- // Vector Green as its primary color?
-
- if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue;
-
- if (!ss.cssRules) continue;
- for (let j = 0; j < ss.cssRules.length; j++) {
- const rule = ss.cssRules[j];
- if (!rule.style) continue;
- for (let k = 0; k < cssAttrs.length; k++) {
- const attr = cssAttrs[k];
- for (let l = 0; l < keyRgb.length; l++) {
- if (rule.style[attr] === keyRgb[l]) {
- cssFixups.push({
- style: rule.style,
- attr: attr,
- index: l,
- });
- }
- }
- }
+ if (color[0] === '#') {
+ color = color.slice(1);
+ if (color.length === 3) {
+ color = color[0] + color[0] +
+ color[1] + color[1] +
+ color[2] + color[2];
+ }
+ const val = parseInt(color, 16);
+ const r = (val >> 16) & 255;
+ const g = (val >> 8) & 255;
+ const b = val & 255;
+ return [r, g, b];
+ } else {
+ const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
+ if (match) {
+ return [
+ parseInt(match[1]),
+ parseInt(match[2]),
+ parseInt(match[3]),
+ ];
}
}
- if (DEBUG) console.log("calcSvgFixups end");
+ return [0, 0, 0];
}
-function applyCssFixups() {
- if (DEBUG) console.log("applyCssFixups start");
- for (let i = 0; i < cssFixups.length; i++) {
- const cssFixup = cssFixups[i];
- cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
- }
- if (DEBUG) console.log("applyCssFixups end");
-}
-
-function hexToRgb(color) {
- if (color[0] === '#') color = color.slice(1);
- if (color.length === 3) {
- color = color[0] + color[0] +
- color[1] + color[1] +
- color[2] + color[2];
- }
- const val = parseInt(color, 16);
- const r = (val >> 16) & 255;
- const g = (val >> 8) & 255;
- const b = val & 255;
- return [r, g, b];
-}
-
-function rgbToHex(rgb) {
+// utility to turn [red,green,blue] into #rrggbb
+function rgbToColor(rgb) {
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
return '#' + (0x1000000 + val).toString(16).slice(1);
}
-// List of functions to call when the tint changes.
-const tintables = [];
+class Tinter {
+ constructor() {
+ // The default colour keys to be replaced as referred to in CSS
+ // (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor)
+ this.keyRgb = [
+ "rgb(118, 207, 166)", // Vector Green
+ "rgb(234, 245, 240)", // Vector Light Green
+ "rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
+ ];
+
+ // Some algebra workings for calculating the tint % of Vector Green & Light Green
+ // x * 118 + (1 - x) * 255 = 234
+ // x * 118 + 255 - 255 * x = 234
+ // x * 118 - x * 255 = 234 - 255
+ // (255 - 118) x = 255 - 234
+ // x = (255 - 234) / (255 - 118) = 0.16
+
+ // The colour keys to be replaced as referred to in SVGs
+ this.keyHex = [
+ "#76CFA6", // Vector Green
+ "#EAF5F0", // Vector Light Green
+ "#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
+ "#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
+ "#000000", // black lowlights of the SVGs (for switching to dark theme)
+ ];
+
+ // track the replacement colours actually being used
+ // defaults to our keys.
+ this.colors = [
+ this.keyHex[0],
+ this.keyHex[1],
+ this.keyHex[2],
+ this.keyHex[3],
+ this.keyHex[4],
+ ];
+
+ // track the most current tint request inputs (which may differ from the
+ // end result stored in this.colors
+ this.currentTint = [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ];
+
+ this.cssFixups = [
+ // { theme: {
+ // style: a style object that should be fixed up taken from a stylesheet
+ // attr: name of the attribute to be clobbered, e.g. 'color'
+ // index: ordinal of primary, secondary or tertiary
+ // },
+ // }
+ ];
+
+ // CSS attributes to be fixed up
+ this.cssAttrs = [
+ "color",
+ "backgroundColor",
+ "borderColor",
+ "borderTopColor",
+ "borderBottomColor",
+ "borderLeftColor",
+ ];
+
+ this.svgAttrs = [
+ "fill",
+ "stroke",
+ ];
+
+ // List of functions to call when the tint changes.
+ this.tintables = [];
+
+ // the currently loaded theme (if any)
+ this.theme = undefined;
+
+ // whether to force a tint (e.g. after changing theme)
+ this.forceTint = false;
+ }
-module.exports = {
/**
* Register a callback to fire when the tint changes.
* This is used to rewrite the tintable SVGs with the new tint.
@@ -167,79 +145,243 @@ module.exports = {
*
* @param {Function} tintable Function to call when the tint changes.
*/
- registerTintable: function(tintable) {
- tintables.push(tintable);
- },
+ registerTintable(tintable) {
+ this.tintables.push(tintable);
+ }
- tint: function(primaryColor, secondaryColor, tertiaryColor) {
- if (!cached) {
- calcCssFixups();
- cached = true;
+ getKeyRgb() {
+ return this.keyRgb;
+ }
+
+ tint(primaryColor, secondaryColor, tertiaryColor) {
+ this.currentTint[0] = primaryColor;
+ this.currentTint[1] = secondaryColor;
+ this.currentTint[2] = tertiaryColor;
+
+ this.calcCssFixups();
+
+ if (DEBUG) {
+ console.log("Tinter.tint(" + primaryColor + ", " +
+ secondaryColor + ", " +
+ tertiaryColor + ")");
}
if (!primaryColor) {
- primaryColor = "#76CFA6"; // Vector green
- secondaryColor = "#EAF5F0"; // Vector light green
+ primaryColor = this.keyRgb[0];
+ secondaryColor = this.keyRgb[1];
+ tertiaryColor = this.keyRgb[2];
}
if (!secondaryColor) {
const x = 0.16; // average weighting factor calculated from vector green & light green
- const rgb = hexToRgb(primaryColor);
+ const rgb = colorToRgb(primaryColor);
rgb[0] = x * rgb[0] + (1 - x) * 255;
rgb[1] = x * rgb[1] + (1 - x) * 255;
rgb[2] = x * rgb[2] + (1 - x) * 255;
- secondaryColor = rgbToHex(rgb);
+ secondaryColor = rgbToColor(rgb);
}
if (!tertiaryColor) {
const x = 0.19;
- const rgb1 = hexToRgb(primaryColor);
- const rgb2 = hexToRgb(secondaryColor);
+ const rgb1 = colorToRgb(primaryColor);
+ const rgb2 = colorToRgb(secondaryColor);
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
- tertiaryColor = rgbToHex(rgb1);
+ tertiaryColor = rgbToColor(rgb1);
}
- if (colors[0] === primaryColor &&
- colors[1] === secondaryColor &&
- colors[2] === tertiaryColor) {
+ if (this.forceTint == false &&
+ this.colors[0] === primaryColor &&
+ this.colors[1] === secondaryColor &&
+ this.colors[2] === tertiaryColor) {
return;
}
- colors[0] = primaryColor;
- colors[1] = secondaryColor;
- colors[2] = tertiaryColor;
+ this.forceTint = false;
- if (DEBUG) console.log("Tinter.tint");
+ this.colors[0] = primaryColor;
+ this.colors[1] = secondaryColor;
+ this.colors[2] = tertiaryColor;
+
+ if (DEBUG) {
+ console.log("Tinter.tint final: (" + primaryColor + ", " +
+ secondaryColor + ", " +
+ tertiaryColor + ")");
+ }
// go through manually fixing up the stylesheets.
- applyCssFixups();
+ this.applyCssFixups();
// tell all the SVGs to go fix themselves up
// we don't do this as a dispatch otherwise it will visually lag
- tintables.forEach(function(tintable) {
+ this.tintables.forEach(function(tintable) {
tintable();
});
- },
+ }
+
+ tintSvgWhite(whiteColor) {
+ this.currentTint[3] = whiteColor;
- tintSvgWhite: function(whiteColor) {
if (!whiteColor) {
- whiteColor = colors[3];
+ whiteColor = this.colors[3];
}
- if (colors[3] === whiteColor) {
+ if (this.colors[3] === whiteColor) {
return;
}
- colors[3] = whiteColor;
- tintables.forEach(function(tintable) {
+ this.colors[3] = whiteColor;
+ this.tintables.forEach(function(tintable) {
tintable();
});
- },
+ }
+
+ tintSvgBlack(blackColor) {
+ this.currentTint[4] = blackColor;
+
+ if (!blackColor) {
+ blackColor = this.colors[4];
+ }
+ if (this.colors[4] === blackColor) {
+ return;
+ }
+ this.colors[4] = blackColor;
+ this.tintables.forEach(function(tintable) {
+ tintable();
+ });
+ }
+
+
+ setTheme(theme) {
+ console.trace("setTheme " + theme);
+ this.theme = theme;
+
+ // update keyRgb from the current theme CSS itself, if it defines it
+ if (document.getElementById('mx_theme_accentColor')) {
+ this.keyRgb[0] = window.getComputedStyle(
+ document.getElementById('mx_theme_accentColor')).color;
+ }
+ if (document.getElementById('mx_theme_secondaryAccentColor')) {
+ this.keyRgb[1] = window.getComputedStyle(
+ document.getElementById('mx_theme_secondaryAccentColor')).color;
+ }
+ if (document.getElementById('mx_theme_tertiaryAccentColor')) {
+ this.keyRgb[2] = window.getComputedStyle(
+ document.getElementById('mx_theme_tertiaryAccentColor')).color;
+ }
+
+ this.calcCssFixups();
+ this.forceTint = true;
+
+ this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]);
+
+ if (theme === 'dark') {
+ // abuse the tinter to change all the SVG's #fff to #2d2d2d
+ // XXX: obviously this shouldn't be hardcoded here.
+ this.tintSvgWhite('#2d2d2d');
+ this.tintSvgBlack('#dddddd');
+ } else {
+ this.tintSvgWhite('#ffffff');
+ this.tintSvgBlack('#000000');
+ }
+ }
+
+ calcCssFixups() {
+ // cache our fixups
+ if (this.cssFixups[this.theme]) return;
+
+ if (DEBUG) {
+ console.debug("calcCssFixups start for " + this.theme + " (checking " +
+ document.styleSheets.length +
+ " stylesheets)");
+ }
+
+ this.cssFixups[this.theme] = [];
+
+ for (let i = 0; i < document.styleSheets.length; i++) {
+ const ss = document.styleSheets[i];
+ if (!ss) continue; // well done safari >:(
+ // Chromium apparently sometimes returns null here; unsure why.
+ // see $14534907369972FRXBx:matrix.org in HQ
+ // ...ah, it's because there's a third party extension like
+ // privacybadger inserting its own stylesheet in there with a
+ // resource:// URI or something which results in a XSS error.
+ // See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
+ // ...except some browsers apparently return stylesheets without
+ // hrefs, which we have no choice but ignore right now
+
+ // XXX seriously? we are hardcoding the name of vector's CSS file in
+ // here?
+ //
+ // Why do we need to limit it to vector's CSS file anyway - if there
+ // are other CSS files affecting the doc don't we want to apply the
+ // same transformations to them?
+ //
+ // Iterating through the CSS looking for matches to hack on feels
+ // pretty horrible anyway. And what if the application skin doesn't use
+ // Vector Green as its primary color?
+ // --richvdh
+
+ // Yes, tinting assumes that you are using the Riot skin for now.
+ // The right solution will be to move the CSS over to react-sdk.
+ // And yes, the default assets for the base skin might as well use
+ // Vector Green as any other colour.
+ // --matthew
+
+ if (ss.href && !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
+ if (ss.disabled) continue;
+ if (!ss.cssRules) continue;
+
+ if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
+
+ for (let j = 0; j < ss.cssRules.length; j++) {
+ const rule = ss.cssRules[j];
+ if (!rule.style) continue;
+ if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
+ for (let k = 0; k < this.cssAttrs.length; k++) {
+ const attr = this.cssAttrs[k];
+ for (let l = 0; l < this.keyRgb.length; l++) {
+ if (rule.style[attr] === this.keyRgb[l]) {
+ this.cssFixups[this.theme].push({
+ style: rule.style,
+ attr: attr,
+ index: l,
+ });
+ }
+ }
+ }
+ }
+ }
+ if (DEBUG) {
+ console.log("calcCssFixups end (" +
+ this.cssFixups[this.theme].length +
+ " fixups)");
+ }
+ }
+
+ applyCssFixups() {
+ if (DEBUG) {
+ console.log("applyCssFixups start (" +
+ this.cssFixups[this.theme].length +
+ " fixups)");
+ }
+ for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
+ const cssFixup = this.cssFixups[this.theme][i];
+ try {
+ cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
+ } catch (e) {
+ // Firefox Quantum explodes if you manually edit the CSS in the
+ // inspector and then try to do a tint, as apparently all the
+ // fixups are then stale.
+ console.error("Failed to apply cssFixup in Tinter! ", e.name);
+ }
+ }
+ if (DEBUG) console.log("applyCssFixups end");
+ }
// XXX: we could just move this all into TintableSvg, but as it's so similar
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
// keeping it here for now.
- calcSvgFixups: function(svgs) {
+ calcSvgFixups(svgs) {
// go through manually fixing up SVG colours.
// we could do this by stylesheets, but keeping the stylesheets
// updated would be a PITA, so just brute-force search for the
@@ -248,10 +390,10 @@ module.exports = {
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
const fixups = [];
for (let i = 0; i < svgs.length; i++) {
- var svgDoc;
+ let svgDoc;
try {
svgDoc = svgs[i].contentDocument;
- } catch(e) {
+ } catch (e) {
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
if (e.message) {
msg += e.message;
@@ -259,16 +401,17 @@ module.exports = {
if (e.stack) {
msg += ' | stack: ' + e.stack;
}
- console.error(e);
+ console.error(msg);
}
if (!svgDoc) continue;
const tags = svgDoc.getElementsByTagName("*");
for (let j = 0; j < tags.length; j++) {
const tag = tags[j];
- for (let k = 0; k < svgAttrs.length; k++) {
- const attr = svgAttrs[k];
- for (let l = 0; l < keyHex.length; l++) {
- if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) {
+ for (let k = 0; k < this.svgAttrs.length; k++) {
+ const attr = this.svgAttrs[k];
+ for (let l = 0; l < this.keyHex.length; l++) {
+ if (tag.getAttribute(attr) &&
+ tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
fixups.push({
node: tag,
attr: attr,
@@ -282,14 +425,19 @@ module.exports = {
if (DEBUG) console.log("calcSvgFixups end");
return fixups;
- },
+ }
- applySvgFixups: function(fixups) {
+ applySvgFixups(fixups) {
if (DEBUG) console.log("applySvgFixups start for " + fixups);
for (let i = 0; i < fixups.length; i++) {
const svgFixup = fixups[i];
- svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
+ svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
}
if (DEBUG) console.log("applySvgFixups end");
- },
-};
+ }
+}
+
+if (global.singletonTinter === undefined) {
+ global.singletonTinter = new Tinter();
+}
+export default global.singletonTinter;
diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js
index 664fe14eb5..e7d77b3b66 100644
--- a/src/UnknownDeviceErrorHandler.js
+++ b/src/UnknownDeviceErrorHandler.js
@@ -25,6 +25,7 @@ const onAction = function(payload) {
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
isDialogOpen = true;
Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
+ devices: payload.err.devices,
room: payload.room,
onFinished: (r) => {
isDialogOpen = false;
diff --git a/src/Unread.js b/src/Unread.js
index 20e876ad88..383b5c2e5a 100644
--- a/src/Unread.js
+++ b/src/Unread.js
@@ -15,7 +15,6 @@ limitations under the License.
*/
const MatrixClientPeg = require('./MatrixClientPeg');
-import UserSettingsStore from './UserSettingsStore';
import shouldHideEvent from './shouldHideEvent';
const sdk = require('./index');
@@ -64,7 +63,6 @@ module.exports = {
// we have and the read receipt. We could fetch more history to try & find out,
// but currently we just guess.
- const syncedSettings = UserSettingsStore.getSyncedSettings();
// Loop through messages, starting with the most recent...
for (let i = room.timeline.length - 1; i >= 0; --i) {
const ev = room.timeline[i];
@@ -74,7 +72,7 @@ module.exports = {
// that counts and we can stop looking because the user's read
// this and everything before.
return false;
- } else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) {
+ } else if (!shouldHideEvent(ev) && this.eventTriggersUnreadCount(ev)) {
// We've found a message that counts before we hit
// the read marker, so this room is definitely unread.
return true;
diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js
index ce39939bc0..5d2af3715f 100644
--- a/src/UserSettingsStore.js
+++ b/src/UserSettingsStore.js
@@ -17,58 +17,11 @@ limitations under the License.
import Promise from 'bluebird';
import MatrixClientPeg from './MatrixClientPeg';
-import Notifier from './Notifier';
-import { _t, _td } from './languageHandler';
-import SdkConfig from './SdkConfig';
/*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
*/
-
-const FEATURES = [
- {
- id: 'feature_groups',
- name: _td("Communities"),
- },
- {
- id: 'feature_pinning',
- name: _td("Message Pinning"),
- },
-];
-
export default {
- getLabsFeatures() {
- const featuresConfig = SdkConfig.get()['features'] || {};
-
- // The old flag: honoured for backwards compatibility
- const enableLabs = SdkConfig.get()['enableLabs'];
-
- let labsFeatures;
- if (enableLabs) {
- labsFeatures = FEATURES;
- } else {
- labsFeatures = FEATURES.filter((f) => {
- const sdkConfigValue = featuresConfig[f.id];
- if (sdkConfigValue === 'labs') {
- return true;
- }
- });
- }
- return labsFeatures.map((f) => {
- return f.id;
- });
- },
-
- translatedNameForFeature(featureId) {
- const feature = FEATURES.filter((f) => {
- return f.id === featureId;
- })[0];
-
- if (feature === undefined) return null;
-
- return _t(feature.name);
- },
-
loadProfileInfo: function() {
const cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId);
@@ -91,36 +44,6 @@ export default {
// TODO
},
- getEnableNotifications: function() {
- return Notifier.isEnabled();
- },
-
- setEnableNotifications: function(enable) {
- if (!Notifier.supportsDesktopNotifications()) {
- return;
- }
- Notifier.setEnabled(enable);
- },
-
- getEnableNotificationBody: function() {
- return Notifier.isBodyEnabled();
- },
-
- setEnableNotificationBody: function(enable) {
- if (!Notifier.supportsDesktopNotifications()) {
- return;
- }
- Notifier.setBodyEnabled(enable);
- },
-
- getEnableAudioNotifications: function() {
- return Notifier.isAudioEnabled();
- },
-
- setEnableAudioNotifications: function(enable) {
- Notifier.setAudioEnabled(enable);
- },
-
changePassword: function(oldPassword, newPassword) {
const cli = MatrixClientPeg.get();
@@ -167,83 +90,4 @@ export default {
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
});
},
-
- getUrlPreviewsDisabled: function() {
- const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls');
- return (event && event.getContent().disable);
- },
-
- setUrlPreviewsDisabled: function(disabled) {
- // FIXME: handle errors
- return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
- disable: disabled,
- });
- },
-
- getSyncedSettings: function() {
- const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
- return event ? event.getContent() : {};
- },
-
- getSyncedSetting: function(type, defaultValue = null) {
- const settings = this.getSyncedSettings();
- return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
- },
-
- setSyncedSetting: function(type, value) {
- const settings = this.getSyncedSettings();
- settings[type] = value;
- // FIXME: handle errors
- return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
- },
-
- getLocalSettings: function() {
- const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
- return JSON.parse(localSettingsString);
- },
-
- getLocalSetting: function(type, defaultValue = null) {
- const settings = this.getLocalSettings();
- return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
- },
-
- setLocalSetting: function(type, value) {
- const settings = this.getLocalSettings();
- settings[type] = value;
- // FIXME: handle errors
- localStorage.setItem('mx_local_settings', JSON.stringify(settings));
- },
-
- isFeatureEnabled: function(featureId: string): boolean {
- const featuresConfig = SdkConfig.get()['features'];
-
- // The old flag: honoured for backwards compatibility
- const enableLabs = SdkConfig.get()['enableLabs'];
-
- let sdkConfigValue = enableLabs ? 'labs' : 'disable';
- if (featuresConfig && featuresConfig[featureId] !== undefined) {
- sdkConfigValue = featuresConfig[featureId];
- }
-
- if (sdkConfigValue === 'enable') {
- return true;
- } else if (sdkConfigValue === 'disable') {
- return false;
- } else if (sdkConfigValue === 'labs') {
- if (!MatrixClientPeg.get().isGuest()) {
- // Make it explicit that guests get the defaults (although they shouldn't
- // have been able to ever toggle the flags anyway)
- const userValue = localStorage.getItem(`mx_labs_feature_${featureId}`);
- return userValue === 'true';
- }
- return false;
- } else {
- console.warn(`Unknown features config for ${featureId}: ${sdkConfigValue}`);
- return false;
- }
- },
-
- setFeatureEnabled: function(featureId: string, enabled: boolean) {
- localStorage.setItem(`mx_labs_feature_${featureId}`, enabled);
- },
};
diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js
new file mode 100644
index 0000000000..74d5b91428
--- /dev/null
+++ b/src/WidgetMessaging.js
@@ -0,0 +1,326 @@
+/*
+Copyright 2017 New Vector Ltd
+
+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.
+*/
+
+/*
+Listens for incoming postMessage requests from embedded widgets. The following API is exposed:
+{
+ api: "widget",
+ action: "content_loaded",
+ widgetId: $WIDGET_ID,
+ data: {}
+ // additional request fields
+}
+
+The complete request object is returned to the caller with an additional "response" key like so:
+{
+ api: "widget",
+ action: "content_loaded",
+ widgetId: $WIDGET_ID,
+ data: {},
+ // additional request fields
+ response: { ... }
+}
+
+The "api" field is required to use this API, and must be set to "widget" in all requests.
+
+The "action" determines the format of the request and response. All actions can return an error response.
+
+Additional data can be sent as additional, abritrary fields. However, typically the data object should be used.
+
+A success response is an object with zero or more keys.
+
+An error response is a "response" object which consists of a sole "error" key to indicate an error.
+They look like:
+{
+ error: {
+ message: "Unable to invite user into room.",
+ _error:
+ }
+}
+The "message" key should be a human-friendly string.
+
+ACTIONS
+=======
+** All actions must include an "api" field with valie "widget".**
+All actions can return an error response instead of the response outlined below.
+
+content_loaded
+--------------
+Indicates that widget contet has fully loaded
+
+Request:
+ - widgetId is the unique ID of the widget instance in riot / matrix state.
+ - No additional fields.
+Response:
+{
+ success: true
+}
+Example:
+{
+ api: "widget",
+ action: "content_loaded",
+ widgetId: $WIDGET_ID
+}
+
+
+api_version
+-----------
+Get the current version of the widget postMessage API
+
+Request:
+ - No additional fields.
+Response:
+{
+ api_version: "0.0.1"
+}
+Example:
+{
+ api: "widget",
+ action: "api_version",
+}
+
+supported_api_versions
+----------------------
+Get versions of the widget postMessage API that are currently supported
+
+Request:
+ - No additional fields.
+Response:
+{
+ api: "widget"
+ supported_versions: ["0.0.1"]
+}
+Example:
+{
+ api: "widget",
+ action: "supported_api_versions",
+}
+
+*/
+
+import URL from 'url';
+
+const WIDGET_API_VERSION = '0.0.1'; // Current API version
+const SUPPORTED_WIDGET_API_VERSIONS = [
+ '0.0.1',
+];
+
+import dis from './dispatcher';
+
+if (!global.mxWidgetMessagingListenerCount) {
+ global.mxWidgetMessagingListenerCount = 0;
+}
+if (!global.mxWidgetMessagingMessageEndpoints) {
+ global.mxWidgetMessagingMessageEndpoints = [];
+}
+
+
+/**
+ * Register widget message event listeners
+ */
+function startListening() {
+ if (global.mxWidgetMessagingListenerCount === 0) {
+ window.addEventListener("message", onMessage, false);
+ }
+ global.mxWidgetMessagingListenerCount += 1;
+}
+
+/**
+ * De-register widget message event listeners
+ */
+function stopListening() {
+ global.mxWidgetMessagingListenerCount -= 1;
+ if (global.mxWidgetMessagingListenerCount === 0) {
+ window.removeEventListener("message", onMessage);
+ }
+ if (global.mxWidgetMessagingListenerCount < 0) {
+ // Make an error so we get a stack trace
+ const e = new Error(
+ "WidgetMessaging: mismatched startListening / stopListening detected." +
+ " Negative count",
+ );
+ console.error(e);
+ }
+}
+
+/**
+ * Register a widget endpoint for trusted postMessage communication
+ * @param {string} widgetId Unique widget identifier
+ * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
+ */
+function addEndpoint(widgetId, endpointUrl) {
+ const u = URL.parse(endpointUrl);
+ if (!u || !u.protocol || !u.host) {
+ console.warn("Invalid origin");
+ return;
+ }
+
+ const origin = u.protocol + '//' + u.host;
+ const endpoint = new WidgetMessageEndpoint(widgetId, origin);
+ if (global.mxWidgetMessagingMessageEndpoints) {
+ if (global.mxWidgetMessagingMessageEndpoints.some(function(ep) {
+ return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
+ })) {
+ // Message endpoint already registered
+ console.warn("Endpoint already registered");
+ return;
+ }
+ global.mxWidgetMessagingMessageEndpoints.push(endpoint);
+ }
+}
+
+/**
+ * De-register a widget endpoint from trusted communication sources
+ * @param {string} widgetId Unique widget identifier
+ * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
+ * @return {boolean} True if endpoint was successfully removed
+ */
+function removeEndpoint(widgetId, endpointUrl) {
+ const u = URL.parse(endpointUrl);
+ if (!u || !u.protocol || !u.host) {
+ console.warn("Invalid origin");
+ return;
+ }
+
+ const origin = u.protocol + '//' + u.host;
+ if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) {
+ const length = global.mxWidgetMessagingMessageEndpoints.length;
+ global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.filter(function(endpoint) {
+ return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
+ });
+ return (length > global.mxWidgetMessagingMessageEndpoints.length);
+ }
+ return false;
+}
+
+
+/**
+ * Handle widget postMessage events
+ * @param {Event} event Event to handle
+ * @return {undefined}
+ */
+function onMessage(event) {
+ if (!event.origin) { // Handle chrome
+ event.origin = event.originalEvent.origin;
+ }
+
+ // Event origin is empty string if undefined
+ if (
+ event.origin.length === 0 ||
+ !trustedEndpoint(event.origin) ||
+ event.data.api !== "widget" ||
+ !event.data.widgetId
+ ) {
+ return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
+ }
+
+ const action = event.data.action;
+ const widgetId = event.data.widgetId;
+ if (action === 'content_loaded') {
+ dis.dispatch({
+ action: 'widget_content_loaded',
+ widgetId: widgetId,
+ });
+ sendResponse(event, {success: true});
+ } else if (action === 'supported_api_versions') {
+ sendResponse(event, {
+ api: "widget",
+ supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
+ });
+ } else if (action === 'api_version') {
+ sendResponse(event, {
+ api: "widget",
+ version: WIDGET_API_VERSION,
+ });
+ } else {
+ console.warn("Widget postMessage event unhandled");
+ sendError(event, {message: "The postMessage was unhandled"});
+ }
+}
+
+/**
+ * Check if message origin is registered as trusted
+ * @param {string} origin PostMessage origin to check
+ * @return {boolean} True if trusted
+ */
+function trustedEndpoint(origin) {
+ if (!origin) {
+ return false;
+ }
+
+ return global.mxWidgetMessagingMessageEndpoints.some((endpoint) => {
+ return endpoint.endpointUrl === origin;
+ });
+}
+
+/**
+ * Send a postmessage response to a postMessage request
+ * @param {Event} event The original postMessage request event
+ * @param {Object} res Response data
+ */
+function sendResponse(event, res) {
+ const data = JSON.parse(JSON.stringify(event.data));
+ data.response = res;
+ event.source.postMessage(data, event.origin);
+}
+
+/**
+ * Send an error response to a postMessage request
+ * @param {Event} event The original postMessage request event
+ * @param {string} msg Error message
+ * @param {Error} nestedError Nested error event (optional)
+ */
+function sendError(event, msg, nestedError) {
+ console.error("Action:" + event.data.action + " failed with message: " + msg);
+ const data = JSON.parse(JSON.stringify(event.data));
+ data.response = {
+ error: {
+ message: msg,
+ },
+ };
+ if (nestedError) {
+ data.response.error._error = nestedError;
+ }
+ event.source.postMessage(data, event.origin);
+}
+
+/**
+ * Represents mapping of widget instance to URLs for trusted postMessage communication.
+ */
+class WidgetMessageEndpoint {
+ /**
+ * Mapping of widget instance to URL for trusted postMessage communication.
+ * @param {string} widgetId Unique widget identifier
+ * @param {string} endpointUrl Widget wurl origin.
+ */
+ constructor(widgetId, endpointUrl) {
+ if (!widgetId) {
+ throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
+ }
+ if (!endpointUrl) {
+ throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
+ }
+ this.widgetId = widgetId;
+ this.endpointUrl = endpointUrl;
+ }
+}
+
+export default {
+ startListening: startListening,
+ stopListening: stopListening,
+ addEndpoint: addEndpoint,
+ removeEndpoint: removeEndpoint,
+};
diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js
index 9f1f40dbe7..f4e576ea0f 100644
--- a/src/autocomplete/EmojiProvider.js
+++ b/src/autocomplete/EmojiProvider.js
@@ -26,7 +26,7 @@ import {PillCompletion} from './Components';
import type {SelectionRange, Completion} from './Autocompleter';
import _uniq from 'lodash/uniq';
import _sortBy from 'lodash/sortBy';
-import UserSettingsStore from '../UserSettingsStore';
+import SettingsStore from "../settings/SettingsStore";
import EmojiData from '../stripped-emoji.json';
@@ -96,7 +96,7 @@ export default class EmojiProvider extends AutocompleteProvider {
}
async getCompletions(query: string, selection: SelectionRange) {
- if (UserSettingsStore.getSyncedSetting("MessageComposerInput.dontSuggestEmoji")) {
+ if (SettingsStore.getValue("MessageComposerInput.dontSuggestEmoji")) {
return []; // don't give any suggestions if the user doesn't want them
}
diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js
index 8b43964b1a..794f507d21 100644
--- a/src/autocomplete/UserProvider.js
+++ b/src/autocomplete/UserProvider.js
@@ -53,8 +53,10 @@ export default class UserProvider extends AutocompleteProvider {
}
destroy() {
- MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
- MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
+ if (MatrixClientPeg.get()) {
+ MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
+ MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
+ }
}
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
@@ -126,7 +128,7 @@ export default class UserProvider extends AutocompleteProvider {
const events = this.room.getLiveTimeline().getEvents();
const lastSpoken = {};
- for(const event of events) {
+ for (const event of events) {
lastSpoken[event.getSender()] = event.getTs();
}
diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js
index c3ad7f9cd1..3c2308e6a7 100644
--- a/src/components/structures/ContextualMenu.js
+++ b/src/components/structures/ContextualMenu.js
@@ -33,6 +33,7 @@ module.exports = {
menuHeight: React.PropTypes.number,
chevronOffset: React.PropTypes.number,
menuColour: React.PropTypes.string,
+ chevronFace: React.PropTypes.string, // top, bottom, left, right
},
getOrCreateContainer: function() {
@@ -58,12 +59,30 @@ module.exports = {
}
};
- const position = {
- top: props.top,
- };
+ const position = {};
+ let chevronFace = null;
+
+ if (props.top) {
+ position.top = props.top;
+ } else {
+ position.bottom = props.bottom;
+ }
+
+ if (props.left) {
+ position.left = props.left;
+ chevronFace = 'left';
+ } else {
+ position.right = props.right;
+ chevronFace = 'right';
+ }
const chevronOffset = {};
- if (props.chevronOffset) {
+ if (props.chevronFace) {
+ chevronFace = props.chevronFace;
+ }
+ if (chevronFace === 'top' || chevronFace === 'bottom') {
+ chevronOffset.left = props.chevronOffset;
+ } else {
chevronOffset.top = props.chevronOffset;
}
@@ -74,28 +93,27 @@ module.exports = {
.mx_ContextualMenu_chevron_left:after {
border-right-color: ${props.menuColour};
}
-
.mx_ContextualMenu_chevron_right:after {
border-left-color: ${props.menuColour};
}
+ .mx_ContextualMenu_chevron_top:after {
+ border-left-color: ${props.menuColour};
+ }
+ .mx_ContextualMenu_chevron_bottom:after {
+ border-left-color: ${props.menuColour};
+ }
`;
}
- let chevron = null;
- if (props.left) {
- chevron = ;
- position.left = props.left;
- } else {
- chevron = ;
- position.right = props.right;
- }
-
+ const chevron = ;
const className = 'mx_ContextualMenu_wrapper';
const menuClasses = classNames({
'mx_ContextualMenu': true,
- 'mx_ContextualMenu_left': props.left,
- 'mx_ContextualMenu_right': !props.left,
+ 'mx_ContextualMenu_left': chevronFace === 'left',
+ 'mx_ContextualMenu_right': chevronFace === 'right',
+ 'mx_ContextualMenu_top': chevronFace === 'top',
+ 'mx_ContextualMenu_bottom': chevronFace === 'bottom',
});
const menuStyle = {};
diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js
index 23feb4cf30..ffa5e45249 100644
--- a/src/components/structures/FilePanel.js
+++ b/src/components/structures/FilePanel.js
@@ -19,7 +19,7 @@ import React from 'react';
import Matrix from 'matrix-js-sdk';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
-import { _t, _tJsx } from '../../languageHandler';
+import { _t } from '../../languageHandler';
/*
* Component which shows the filtered file using a TimelinePanel
@@ -92,7 +92,10 @@ const FilePanel = React.createClass({
if (MatrixClientPeg.get().isGuest()) {
return
- { _t("%(inviter)s has invited you to join this community", {inviter: group.inviter.userId}) }
+
+ { _t("%(inviter)s has invited you to join this community", {
+ inviter: inviterName,
+ }) }
-
{ _t("Community Member Settings") }
-
-
-
-
-
;
- },
-
_getLongDescriptionNode: function() {
const summary = this.state.summary;
let description = null;
@@ -889,12 +913,12 @@ export default React.createClass({
className="mx_GroupView_groupDesc_placeholder"
onClick={this._onEditClick}
>
- { _tJsx(
+ { _t(
'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!',
- [/ /],
- [(sub) => ])
- }
+ {},
+ { 'br': },
+ ) }
;
}
const groupDescEditingClasses = classnames({
@@ -933,7 +957,6 @@ export default React.createClass({
let shortDescNode;
const bodyNodes = [
this._getMembershipSection(),
- this.state.editing ? this._getMemberSettingsSection() : null,
this._getGroupSection(),
];
const rightButtons = [];
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index 08120d9508..01abf966f9 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -19,8 +19,7 @@ limitations under the License.
import * as Matrix from 'matrix-js-sdk';
import React from 'react';
-import UserSettingsStore from '../../UserSettingsStore';
-import KeyCode from '../../KeyCode';
+import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import Notifier from '../../Notifier';
import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
@@ -28,6 +27,7 @@ import sdk from '../../index';
import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore';
import MatrixClientPeg from '../../MatrixClientPeg';
+import SettingsStore from "../../settings/SettingsStore";
/**
* This is what our MatrixChat shows when we are logged in. The precise view is
@@ -74,7 +74,7 @@ export default React.createClass({
getInitialState: function() {
return {
// use compact timeline view
- useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
+ useCompactLayout: SettingsStore.getValue('useCompactLayout'),
};
},
@@ -153,13 +153,7 @@ export default React.createClass({
*/
let handled = false;
- const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
- let ctrlCmdOnly;
- if (isMac) {
- ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
- } else {
- ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
- }
+ const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
switch (ev.keyCode) {
case KeyCode.UP:
@@ -213,6 +207,7 @@ export default React.createClass({
},
render: function() {
+ const TagPanel = sdk.getComponent('structures.TagPanel');
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RightPanel = sdk.getComponent('structures.RightPanel');
const RoomView = sdk.getComponent('structures.RoomView');
@@ -334,6 +329,7 @@ export default React.createClass({
{ topBar }
+ { SettingsStore.isFeatureEnabled("feature_tag_panel") ? : }
{
- if(loggedIn) {
+ if (loggedIn) {
this.props.onTokenLoginCompleted();
// don't do anything else until the page reloads - just stay in
@@ -374,6 +389,22 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ // Start the onboarding process for certain actions
+ if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest() &&
+ ONBOARDING_FLOW_STARTERS.includes(payload.action)
+ ) {
+ // This will cause `payload` to be dispatched later, once a
+ // sync has reached the "prepared" state. Setting a matrix ID
+ // will cause a full login and sync and finally the deferred
+ // action will be dispatched.
+ dis.dispatch({
+ action: 'do_after_sync_prepared',
+ deferred_action: payload,
+ });
+ dis.dispatch({action: 'view_set_mxid'});
+ return;
+ }
+
switch (payload.action) {
case 'logout':
Lifecycle.logout();
@@ -463,22 +494,17 @@ module.exports = React.createClass({
this._viewIndexedRoom(payload.roomIndex);
break;
case 'view_user_settings':
- if (MatrixClientPeg.get().isGuest()) {
- dis.dispatch({
- action: 'do_after_sync_prepared',
- deferred_action: {
- action: 'view_user_settings',
- },
- });
- dis.dispatch({action: 'view_set_mxid'});
- break;
- }
this._setPage(PageTypes.UserSettings);
this.notifyNewScreen('settings');
break;
case 'view_create_room':
this._createRoom();
break;
+ case 'view_create_group': {
+ const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
+ Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
+ }
+ break;
case 'view_room_directory':
this._setPage(PageTypes.RoomDirectory);
this.notifyNewScreen('directory');
@@ -509,7 +535,7 @@ module.exports = React.createClass({
this._chatCreateOrReuse(payload.user_id, payload.go_home_on_cancel);
break;
case 'view_create_chat':
- this._createChat();
+ showStartChatInviteDialog();
break;
case 'view_invite':
showRoomInviteDialog(payload.roomId);
@@ -569,6 +595,9 @@ module.exports = React.createClass({
this._onWillStartClient();
});
break;
+ case 'client_started':
+ this._onClientStarted();
+ break;
case 'new_version':
this.onVersion(
payload.currentVersion, payload.newVersion,
@@ -750,31 +779,7 @@ module.exports = React.createClass({
}).close;
},
- _createChat: function() {
- if (MatrixClientPeg.get().isGuest()) {
- dis.dispatch({
- action: 'do_after_sync_prepared',
- deferred_action: {
- action: 'view_create_chat',
- },
- });
- dis.dispatch({action: 'view_set_mxid'});
- return;
- }
- showStartChatInviteDialog();
- },
-
_createRoom: function() {
- if (MatrixClientPeg.get().isGuest()) {
- dis.dispatch({
- action: 'do_after_sync_prepared',
- deferred_action: {
- action: 'view_create_room',
- },
- });
- dis.dispatch({action: 'view_set_mxid'});
- return;
- }
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
onFinished: (shouldCreate, name, noFederate) => {
@@ -890,7 +895,7 @@ module.exports = React.createClass({
*/
_onSetTheme: function(theme) {
if (!theme) {
- theme = 'light';
+ theme = SettingsStore.getValue("theme");
}
// look for the stylesheet elements.
@@ -913,18 +918,49 @@ module.exports = React.createClass({
// disable all of them first, then enable the one we want. Chrome only
// bothers to do an update on a true->false transition, so this ensures
// that we get exactly one update, at the right time.
+ //
+ // ^ This comment was true when we used to use alternative stylesheets
+ // for the CSS. Nowadays we just set them all as disabled in index.html
+ // and enable them as needed. It might be cleaner to disable them all
+ // at the same time to prevent loading two themes simultaneously and
+ // having them interact badly... but this causes a flash of unstyled app
+ // which is even uglier. So we don't.
- Object.values(styleElements).forEach((a) => {
- a.disabled = true;
- });
styleElements[theme].disabled = false;
- if (theme === 'dark') {
- // abuse the tinter to change all the SVG's #fff to #2d2d2d
- // XXX: obviously this shouldn't be hardcoded here.
- Tinter.tintSvgWhite('#2d2d2d');
- } else {
- Tinter.tintSvgWhite('#ffffff');
+ const switchTheme = function() {
+ // we re-enable our theme here just in case we raced with another
+ // theme set request as per https://github.com/vector-im/riot-web/issues/5601.
+ // We could alternatively lock or similar to stop the race, but
+ // this is probably good enough for now.
+ styleElements[theme].disabled = false;
+ Object.values(styleElements).forEach((a) => {
+ if (a == styleElements[theme]) return;
+ a.disabled = true;
+ });
+ Tinter.setTheme(theme);
+ };
+
+ // turns out that Firefox preloads the CSS for link elements with
+ // the disabled attribute, but Chrome doesn't.
+
+ let cssLoaded = false;
+
+ styleElements[theme].onload = () => {
+ switchTheme();
+ };
+
+ for (let i = 0; i < document.styleSheets.length; i++) {
+ const ss = document.styleSheets[i];
+ if (ss && ss.href === styleElements[theme].href) {
+ cssLoaded = true;
+ break;
+ }
+ }
+
+ if (cssLoaded) {
+ styleElements[theme].onload = undefined;
+ switchTheme();
}
},
@@ -1095,6 +1131,34 @@ module.exports = React.createClass({
cli.on("crypto.roomKeyRequestCancellation", (req) => {
krh.handleKeyRequestCancellation(req);
});
+ cli.on("Room", (room) => {
+ if (MatrixClientPeg.get().isCryptoEnabled()) {
+ const blacklistEnabled = SettingsStore.getValueAt(
+ SettingLevel.ROOM_DEVICE,
+ "blacklistUnverifiedDevices",
+ room.roomId,
+ /*explicit=*/true,
+ );
+ room.setBlacklistUnverifiedDevices(blacklistEnabled);
+ }
+ });
+ },
+
+ /**
+ * Called shortly after the matrix client has started. Useful for
+ * setting up anything that requires the client to be started.
+ * @private
+ */
+ _onClientStarted: function() {
+ const cli = MatrixClientPeg.get();
+
+ if (cli.isCryptoEnabled()) {
+ const blacklistEnabled = SettingsStore.getValueAt(
+ SettingLevel.DEVICE,
+ "blacklistUnverifiedDevices",
+ );
+ cli.setGlobalBlacklistUnverifiedDevices(blacklistEnabled);
+ }
},
showScreen: function(screen, params) {
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 2331e096c0..53cc660a9b 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
-import UserSettingsStore from '../../UserSettingsStore';
import shouldHideEvent from '../../shouldHideEvent';
import dis from "../../dispatcher";
import sdk from '../../index';
@@ -110,8 +109,6 @@ module.exports = React.createClass({
// Velocity requires
this._readMarkerGhostNode = null;
- this._syncedSettings = UserSettingsStore.getSyncedSettings();
-
this._isMounted = true;
},
@@ -251,7 +248,7 @@ module.exports = React.createClass({
// Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true;
- return !shouldHideEvent(mxEv, this._syncedSettings);
+ return !shouldHideEvent(mxEv);
},
_getEventTiles: function() {
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index cc4783fdac..9281fb199e 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -15,69 +15,12 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar';
-import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../index';
-import { _t, _tJsx } from '../../languageHandler';
+import { _t } from '../../languageHandler';
+import dis from '../../dispatcher';
import withMatrixClient from '../../wrappers/withMatrixClient';
import AccessibleButton from '../views/elements/AccessibleButton';
-import dis from '../../dispatcher';
-import Modal from '../../Modal';
-
-import FlairStore from '../../stores/FlairStore';
-
-const GroupTile = React.createClass({
- displayName: 'GroupTile',
-
- propTypes: {
- groupId: PropTypes.string.isRequired,
- },
-
- contextTypes: {
- matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
- },
-
- getInitialState() {
- return {
- profile: null,
- };
- },
-
- componentWillMount: function() {
- FlairStore.getGroupProfileCached(this.context.matrixClient, this.props.groupId).then((profile) => {
- this.setState({profile});
- });
- },
-
- onClick: function(e) {
- e.preventDefault();
- dis.dispatch({
- action: 'view_group',
- group_id: this.props.groupId,
- });
- },
-
- render: function() {
- const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
- const profile = this.state.profile || {};
- const name = profile.name || this.props.groupId;
- const desc = profile.shortDescription;
- const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
- profile.avatarUrl, 50, 50, "crop",
- ) : null;
- return
-
- { _tJsx(
+ { _t(
'To join an existing community you\'ll have to '+
'know its community identifier; this will look '+
'something like +example:matrix.org.',
- /(.*)<\/i>/,
- (sub) => { sub },
- ) }
+ {},
+ { 'i': (sub) => { sub } })
+ }
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index cad55351d1..03859f522e 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -15,13 +15,12 @@ limitations under the License.
*/
import React from 'react';
-import { _t, _tJsx } from '../../languageHandler';
+import { _t } from '../../languageHandler';
import sdk from '../../index';
import WhoIsTyping from '../../WhoIsTyping';
import MatrixClientPeg from '../../MatrixClientPeg';
import MemberAvatar from '../views/avatars/MemberAvatar';
-const HIDE_DEBOUNCE_MS = 10000;
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
const STATUS_BAR_EXPANDED_LARGE = 2;
@@ -272,12 +271,16 @@ module.exports = React.createClass({
{ this.props.unsentMessageError }
- { _tJsx("Resend all or cancel all now. You can also select individual messages to resend or cancel.",
- [/(.*?)<\/a>/, /(.*?)<\/a>/],
- [
- (sub) => { sub },
- (sub) => { sub },
- ],
+ {
+ _t("Resend all or cancel all now. " +
+ "You can also select individual messages to resend or cancel.",
+ {},
+ {
+ 'resendText': (sub) =>
+ { sub },
+ 'cancelText': (sub) =>
+ { sub },
+ },
) }
+ );
},
- _onPreviewsDisabledChanged: function(e) {
- UserSettingsStore.setUrlPreviewsDisabled(e.target.checked);
- },
-
- _renderSyncedSetting: function(setting) {
- // TODO: this ought to be a separate component so that we don't need
- // to rebind the onChange each time we render
-
- const onChange = (e) => {
- UserSettingsStore.setSyncedSetting(setting.id, e.target.checked);
- if (setting.fn) setting.fn(e.target.checked);
- };
-
- return
-
-
-
;
- },
-
- _renderThemeSelector: function(setting) {
- // TODO: this ought to be a separate component so that we don't need
- // to rebind the onChange each time we render
- const onChange = (e) => {
- if (e.target.checked) {
- this._syncedSettings[setting.id] = setting.value;
- UserSettingsStore.setSyncedSetting(setting.id, setting.value);
- }
- dis.dispatch({
- action: 'set_theme',
- value: setting.value,
- });
- };
- return
);
@@ -873,24 +765,16 @@ module.exports = React.createClass({
} else return ();
},
- _renderLocalSetting: function(setting) {
- // TODO: this ought to be a separate component so that we don't need
- // to rebind the onChange each time we render
- const onChange = (e) => {
- UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
- if (setting.fn) setting.fn(e.target.checked);
- };
-
- return
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
- { ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting ) }
+ { ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
;
},
_renderLabs: function() {
const features = [];
- UserSettingsStore.getLabsFeatures().forEach((featureId) => {
+ SettingsStore.getLabsFeatures().forEach((featureId) => {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = (e) => {
- UserSettingsStore.setFeatureEnabled(featureId, e.target.checked);
+ SettingsStore.setFeatureEnabled(featureId, e.target.checked);
this.forceUpdate();
};
@@ -948,10 +832,10 @@ module.exports = React.createClass({
type="checkbox"
id={featureId}
name={featureId}
- defaultChecked={UserSettingsStore.isFeatureEnabled(featureId)}
+ defaultChecked={SettingsStore.isFeatureEnabled(featureId)}
onChange={onChange}
/>
-
+
);
});
@@ -1044,6 +928,8 @@ module.exports = React.createClass({
const settings = this.state.electron_settings;
if (!settings) return;
+ // TODO: This should probably be a granular setting, but it only applies to electron
+ // and ends up being get/set outside of matrix anyways (local system setting).
return
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
+
{ _t('Your password has been reset') }.
{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.
+
);
},
});
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index 8ee6eafad4..9ed710534b 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.js
@@ -18,12 +18,13 @@ limitations under the License.
'use strict';
import React from 'react';
-import { _t, _tJsx } from '../../../languageHandler';
+import { _t } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler';
import sdk from '../../../index';
import Login from '../../../Login';
-import UserSettingsStore from '../../../UserSettingsStore';
import PlatformPeg from '../../../PlatformPeg';
+import SdkConfig from '../../../SdkConfig';
+import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
@@ -76,6 +77,14 @@ module.exports = React.createClass({
componentWillMount: function() {
this._unmounted = false;
+
+ // map from login step type to a function which will render a control
+ // letting you do that login type
+ this._stepRendererMap = {
+ 'm.login.password': this._renderPasswordStep,
+ 'm.login.cas': this._renderCasStep,
+ };
+
this._initLoginLogic();
},
@@ -95,7 +104,7 @@ module.exports = React.createClass({
).then((data) => {
this.props.onLoggedIn(data);
}, (error) => {
- if(this._unmounted) {
+ if (this._unmounted) {
return;
}
let errorText;
@@ -105,7 +114,22 @@ module.exports = React.createClass({
if (error.httpStatus == 400 && usingEmail) {
errorText = _t('This Home Server does not support login using email address.');
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
- errorText = _t('Incorrect username and/or password.');
+ if (SdkConfig.get().disable_custom_urls) {
+ errorText = (
+
+
{ _t('Incorrect username and/or password.') }
+
+ { _t('Please note you are logging into the %(hs)s server, not matrix.org.',
+ {
+ hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
+ })
+ }
+
+
+ );
+ } else {
+ errorText = _t('Incorrect username and/or password.');
+ }
} else {
// other errors, not specific to doing a password login
errorText = this._errorTextFromError(error);
@@ -120,7 +144,7 @@ module.exports = React.createClass({
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
});
}).finally(() => {
- if(this._unmounted) {
+ if (this._unmounted) {
return;
}
this.setState({
@@ -217,13 +241,29 @@ module.exports = React.createClass({
loginIncorrect: false,
});
- loginLogic.getFlows().then(function(flows) {
- // old behaviour was to always use the first flow without presenting
- // options. This works in most cases (we don't have a UI for multiple
- // logins so let's skip that for now).
- loginLogic.chooseFlow(0);
- self.setState({
- currentFlow: self._getCurrentFlowStep(),
+ loginLogic.getFlows().then((flows) => {
+ // look for a flow where we understand all of the steps.
+ for (let i = 0; i < flows.length; i++ ) {
+ if (!this._isSupportedFlow(flows[i])) {
+ continue;
+ }
+
+ // we just pick the first flow where we support all the
+ // steps. (we don't have a UI for multiple logins so let's skip
+ // that for now).
+ loginLogic.chooseFlow(i);
+ this.setState({
+ currentFlow: this._getCurrentFlowStep(),
+ });
+ return;
+ }
+ // we got to the end of the list without finding a suitable
+ // flow.
+ this.setState({
+ errorText: _t(
+ "This homeserver doesn't offer any login flows which are " +
+ "supported by this client.",
+ ),
});
}, function(err) {
self.setState({
@@ -237,6 +277,16 @@ module.exports = React.createClass({
}).done();
},
+ _isSupportedFlow: function(flow) {
+ // technically the flow can have multiple steps, but no one does this
+ // for login and loginLogic doesn't support it so we can ignore it.
+ if (!this._stepRendererMap[flow.type]) {
+ console.log("Skipping flow", flow, "due to unsupported login type", flow.type);
+ return false;
+ }
+ return true;
+ },
+
_getCurrentFlowStep: function() {
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
},
@@ -256,17 +306,19 @@ module.exports = React.createClass({
!this.state.enteredHomeserverUrl.startsWith("http"))
) {
errorText =
- { _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
+ {
+ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
"Either use HTTPS or enable unsafe scripts.",
- /(.*?)<\/a>/,
- (sub) => { return { sub }; },
+ {},
+ { 'a': (sub) => { return { sub }; } },
) }
;
} else {
errorText =
- { _tJsx("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.",
- /(.*?)<\/a>/,
- (sub) => { return { sub }; },
+ {
+ _t("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.",
+ {},
+ { 'a': (sub) => { return { sub }; } },
) }
;
}
@@ -276,43 +328,47 @@ module.exports = React.createClass({
},
componentForStep: function(step) {
- switch (step) {
- case 'm.login.password':
- const PasswordLogin = sdk.getComponent('login.PasswordLogin');
- return (
-
- );
- case 'm.login.cas':
- const CasLogin = sdk.getComponent('login.CasLogin');
- return (
-
- );
- default:
- if (!step) {
- return;
- }
- return (
-
- { _t('Sorry, this homeserver is using a login which is not recognised ') }({ step })
-
);
}
let returnToAppJsx;
+ /*
+ // with the advent of ILAG I don't think we need this any more
if (this.props.onCancelClick) {
returnToAppJsx = (
@@ -384,8 +392,31 @@ module.exports = React.createClass({
);
}
+ */
+
+ let header;
+ let errorText;
+ // FIXME: remove hardcoded Status team tweaks at some point
+ if (theme === 'status' && this.state.errorText) {
+ header =
+ ;
+ },
+});
+
+export default GroupTile;
diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js
new file mode 100644
index 0000000000..755d6aae8f
--- /dev/null
+++ b/src/components/views/groups/GroupUserSettings.js
@@ -0,0 +1,89 @@
+/*
+Copyright 2017 New Vector Ltd
+
+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 GeminiScrollbar from 'react-gemini-scrollbar';
+import sdk from '../../../index';
+import { MatrixClient } from 'matrix-js-sdk';
+import { _t } from '../../../languageHandler';
+
+export default React.createClass({
+ displayName: 'GroupUserSettings',
+
+ contextTypes: {
+ matrixClient: PropTypes.instanceOf(MatrixClient),
+ },
+
+ getInitialState() {
+ return {
+ error: null,
+ groups: null,
+ };
+ },
+
+ componentWillMount: function() {
+ this.context.matrixClient.getJoinedGroups().done((result) => {
+ this.setState({groups: result.groups || [], error: null});
+ }, (err) => {
+ console.error(err);
+ this.setState({groups: null, error: err});
+ });
+ },
+
+ _renderGroupPublicity() {
+ let text = "";
+ let scrollbox = ;
+ const groups = this.state.groups;
+
+ if (this.state.error) {
+ text = _t('Something went wrong when trying to get your communities.');
+ } else if (groups === null) {
+ text = _t('Loading...');
+ } else if (groups.length > 0) {
+ const GroupPublicityToggle = sdk.getComponent('groups.GroupPublicityToggle');
+ const groupPublicityToggles = groups.map((groupId, index) => {
+ return ;
+ });
+ text = _t('Display your community flair in rooms configured to show it.');
+ scrollbox =
+
+ { groupPublicityToggles }
+
+
;
+ } else {
+ text = _t("You're not currently a member of any communities.");
+ }
+
+ return
;
+ },
+});
diff --git a/src/components/views/login/CaptchaForm.js b/src/components/views/login/CaptchaForm.js
index cf814b0a6e..21e5094b28 100644
--- a/src/components/views/login/CaptchaForm.js
+++ b/src/components/views/login/CaptchaForm.js
@@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
-import { _t, _tJsx } from '../../../languageHandler';
+import { _t } from '../../../languageHandler';
const DIV_ID = 'mx_recaptcha';
@@ -67,10 +67,10 @@ module.exports = React.createClass({
// * jumping straight to a hosted captcha page (but we don't support that yet)
// * embedding the captcha in an iframe (if that works)
// * using a better captcha lib
- ReactDOM.render(_tJsx(
+ ReactDOM.render(_t(
"Robot check is currently unavailable on desktop - please use a web browser",
- /(.*?)<\/a>/,
- (sub) => { return { sub }; }), warning);
+ {},
+ { 'a': (sub) => { return { sub }; }}), warning);
this.refs.recaptchaContainer.appendChild(warning);
} else {
const scriptTag = document.createElement('script');
diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js
index 5f5a74ccd1..d0b6c8decb 100644
--- a/src/components/views/login/InteractiveAuthEntryComponents.js
+++ b/src/components/views/login/InteractiveAuthEntryComponents.js
@@ -20,7 +20,7 @@ import url from 'url';
import classnames from 'classnames';
import sdk from '../../../index';
-import { _t, _tJsx } from '../../../languageHandler';
+import { _t } from '../../../languageHandler';
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@@ -256,7 +256,10 @@ export const EmailIdentityAuthEntry = React.createClass({
} else {
return (
-
{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => {this.props.inputs.emailAddress}) }
+
{ _t("An email has been sent to %(emailAddress)s",
+ { emailAddress: (sub) => { this.props.inputs.emailAddress } },
+ ) }
+
{ _t("Please check your email to continue registration.") }
{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => {this._msisdn}) }
+
{ _t("A text message has been sent to %(msisdn)s",
+ { msisdn: this._msisdn },
+ ) }
+
{ _t("Please enter the code it contains:") }
),
onFinished: (shouldUpload) => {
- if(shouldUpload) {
+ if (shouldUpload) {
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
if (files) {
- for(let i=0; i
-
+
,
);
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index aa019de091..cd30f20645 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -28,14 +28,13 @@ import Promise from 'bluebird';
import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import SlashCommands from '../../../SlashCommands';
-import KeyCode from '../../../KeyCode';
+import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import Analytics from '../../../Analytics';
import dis from '../../../dispatcher';
-import UserSettingsStore from '../../../UserSettingsStore';
import * as RichText from '../../../RichText';
import * as HtmlUtils from '../../../HtmlUtils';
@@ -50,6 +49,7 @@ const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
+import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
@@ -105,13 +105,7 @@ export default class MessageComposerInput extends React.Component {
};
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
- const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
- let ctrlCmdOnly;
- if (isMac) {
- ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
- } else {
- ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
- }
+ const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
// Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
// importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not
@@ -165,7 +159,7 @@ export default class MessageComposerInput extends React.Component {
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
this.onTextPasted = this.onTextPasted.bind(this);
- const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
+ const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
Analytics.setRichtextMode(isRichtextEnabled);
@@ -216,7 +210,7 @@ export default class MessageComposerInput extends React.Component {
createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
RichText.getScopedMDDecorators(this.props);
- const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
+ const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
decorators.push({
strategy: this.findPillEntities.bind(this),
component: (entityProps) => {
@@ -384,7 +378,7 @@ export default class MessageComposerInput extends React.Component {
}
sendTyping(isTyping) {
- if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
+ if (SettingsStore.getValue('dontSendTypingNotifications')) return;
MatrixClientPeg.get().sendTyping(
this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT,
@@ -431,10 +425,10 @@ export default class MessageComposerInput extends React.Component {
}
// Automatic replacement of plaintext emoji to Unicode emoji
- if (UserSettingsStore.getSyncedSetting('MessageComposerInput.autoReplaceEmoji', false)) {
+ if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
// The first matched group includes just the matched plaintext emoji
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
- if(emojiMatch) {
+ if (emojiMatch) {
// plaintext -> hex unicode
const emojiUc = asciiList[emojiMatch[1]];
// hex unicode -> shortname -> actual unicode
@@ -551,7 +545,7 @@ export default class MessageComposerInput extends React.Component {
editorState: this.createEditorState(enabled, contentState),
isRichtextEnabled: enabled,
});
- UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled);
+ SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
}
handleKeyCommand = (command: string): boolean => {
@@ -696,7 +690,7 @@ export default class MessageComposerInput extends React.Component {
}
const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState);
- if(
+ if (
['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item']
.includes(currentBlockType)
) {
diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js
index 69ba7c39c3..b6e171100b 100644
--- a/src/components/views/rooms/PresenceLabel.js
+++ b/src/components/views/rooms/PresenceLabel.js
@@ -44,6 +44,8 @@ module.exports = React.createClass({
};
},
+ // Return duration as a string using appropriate time units
+ // XXX: This would be better handled using a culture-aware library, but we don't use one yet.
getDuration: function(time) {
if (!time) return;
const t = parseInt(time / 1000);
@@ -53,41 +55,39 @@ module.exports = React.createClass({
const d = parseInt(t / (60 * 60 * 24));
if (t < 60) {
if (t < 0) {
- return _t("for %(amount)ss", {amount: 0});
+ return _t("%(duration)ss", {duration: 0});
}
- return _t("for %(amount)ss", {amount: s});
+ return _t("%(duration)ss", {duration: s});
}
if (t < 60 * 60) {
- return _t("for %(amount)sm", {amount: m});
+ return _t("%(duration)sm", {duration: m});
}
if (t < 24 * 60 * 60) {
- return _t("for %(amount)sh", {amount: h});
+ return _t("%(duration)sh", {duration: h});
}
- return _t("for %(amount)sd", {amount: d});
+ return _t("%(duration)sd", {duration: d});
},
- getPrettyPresence: function(presence) {
- if (presence === "online") return _t("Online");
- if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
- if (presence === "offline") return _t("Offline");
- return _t("Unknown");
+ getPrettyPresence: function(presence, activeAgo, currentlyActive) {
+ if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
+ const duration = this.getDuration(activeAgo);
+ if (presence === "online") return _t("Online for %(duration)s", { duration: duration });
+ if (presence === "unavailable") return _t("Idle for %(duration)s", { duration: duration }); // XXX: is this actually right?
+ if (presence === "offline") return _t("Offline for %(duration)s", { duration: duration });
+ return _t("Unknown for %(duration)s", { duration: duration });
+ } else {
+ if (presence === "online") return _t("Online");
+ if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
+ if (presence === "offline") return _t("Offline");
+ return _t("Unknown");
+ }
},
render: function() {
- if (this.props.activeAgo >= 0) {
- const duration = this.getDuration(this.props.activeAgo);
- const ago = this.props.currentlyActive || !duration ? "" : duration;
- return (
-
- { this.getPrettyPresence(this.props.presenceState) } { ago }
-
- { _tJsx(
+ { _t(
"You're not in any rooms yet! Press to make a room or"+
" to browse the directory",
- [//, //],
- [
- (sub) => ,
- (sub) => ,
- ],
+ {},
+ {
+ 'CreateRoomButton': ,
+ 'RoomDirectoryButton': ,
+ },
) }
;
}
diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js
index 0c0601a504..175a3ea552 100644
--- a/src/components/views/rooms/RoomPreviewBar.js
+++ b/src/components/views/rooms/RoomPreviewBar.js
@@ -21,7 +21,7 @@ const React = require('react');
const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
-import { _t, _tJsx } from '../../../languageHandler';
+import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'RoomPreviewBar',
@@ -135,13 +135,13 @@ module.exports = React.createClass({
{ _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) }
- { _tJsx(
+ { _t(
'Would you like to accept or decline this invitation?',
- [/(.*?)<\/acceptText>/, /(.*?)<\/declineText>/],
- [
- (sub) => { sub },
- (sub) => { sub },
- ],
+ {},
+ {
+ 'acceptText': (sub) => { sub },
+ 'declineText': (sub) => { sub },
+ },
) }
{ emailMatchBlock }
@@ -165,13 +165,13 @@ module.exports = React.createClass({
let actionText;
if (kicked) {
- if(roomName) {
+ if (roomName) {
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
} else {
actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName});
}
} else if (banned) {
- if(roomName) {
+ if (roomName) {
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
} else {
actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName});
@@ -211,9 +211,9 @@ module.exports = React.createClass({
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
- { _tJsx("Click here to join the discussion!",
- /(.*?)<\/a>/,
- (sub) => { sub },
+ { _t("Click here to join the discussion!",
+ {},
+ { 'a': (sub) => { sub } },
) }
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index 2f46a9308e..4ac2da2030 100644
--- a/src/components/views/rooms/RoomSettings.js
+++ b/src/components/views/rooms/RoomSettings.js
@@ -17,14 +17,14 @@ limitations under the License.
import Promise from 'bluebird';
import React from 'react';
-import { _t, _tJsx, _td } from '../../../languageHandler';
+import { _t, _td } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import Modal from '../../../Modal';
import ObjectUtils from '../../../ObjectUtils';
import dis from '../../../dispatcher';
-import UserSettingsStore from '../../../UserSettingsStore';
import AccessibleButton from '../elements/AccessibleButton';
+import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
// parse a string as an integer; if the input is undefined, or cannot be parsed
@@ -311,7 +311,7 @@ module.exports = React.createClass({
// url preview settings
const ps = this.saveUrlPreviewSettings();
if (ps.length > 0) {
- promises.push(ps);
+ ps.map((p) => promises.push(p));
}
// related groups
@@ -363,26 +363,16 @@ module.exports = React.createClass({
},
saveBlacklistUnverifiedDevicesPerRoom: function() {
- if (!this.refs.blacklistUnverified) return;
- if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) {
- this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked);
- }
- },
-
- _isRoomBlacklistUnverified: function() {
- const blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom;
- if (blacklistUnverifiedDevicesPerRoom) {
- return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId];
- }
- return false;
- },
-
- _setRoomBlacklistUnverified: function(value) {
- const blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom || {};
- blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value;
- UserSettingsStore.setLocalSetting('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom);
-
- this.props.room.setBlacklistUnverifiedDevices(value);
+ if (!this.refs.blacklistUnverifiedDevices) return;
+ this.refs.blacklistUnverifiedDevices.save().then(() => {
+ const value = SettingsStore.getValueAt(
+ SettingLevel.ROOM_DEVICE,
+ "blacklistUnverifiedDevices",
+ this.props.room.roomId,
+ /*explicit=*/true,
+ );
+ this.props.room.setBlacklistUnverifiedDevices(value);
+ });
},
_hasDiff: function(strA, strB) {
@@ -588,19 +578,20 @@ module.exports = React.createClass({
},
_renderEncryptionSection: function() {
+ const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
+
const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState;
const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
- const isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices;
- const isRoomBlacklistUnverified = this._isRoomBlacklistUnverified();
- const settings =
- ;
+ const settings = (
+
+ );
if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
return (
@@ -637,9 +628,7 @@ module.exports = React.createClass({
const ColorSettings = sdk.getComponent("room_settings.ColorSettings");
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
- const EditableText = sdk.getComponent('elements.EditableText');
const PowerSelector = sdk.getComponent('elements.PowerSelector');
- const Loader = sdk.getComponent("elements.Spinner");
const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState;
@@ -671,13 +660,11 @@ module.exports = React.createClass({
const self = this;
- let relatedGroupsSection;
- if (UserSettingsStore.isFeatureEnabled('feature_groups')) {
- relatedGroupsSection = ;
- }
+ const relatedGroupsSection = ;
let userLevelsSection;
if (Object.keys(user_levels).length) {
@@ -760,9 +747,9 @@ module.exports = React.createClass({
}
});
- var tagsSection = null;
+ let tagsSection = null;
if (canSetTag || self.state.tags) {
- var tagsSection =
+ tagsSection =
{ _t('The default role for new room members is') }
-
+
{ _t('To send messages, you must be a') }
-
+
{ _t('To invite users into the room, you must be a') }
-
+
{ _t('To configure the room, you must be a') }
-
+
{ _t('To kick users, you must be a') }
-
+
{ _t('To ban users, you must be a') }
-
+
{ _t('To remove other users\' messages, you must be a') }
-
+
{ Object.keys(events_levels).map(function(event_type, i) {
let label = plEventsToLabels[event_type];
if (label) label = _t(label);
- else label = _tJsx("To send events of type , you must be a", //, () => { event_type });
+ else label = _t("To send events of type , you must be a", {}, { 'eventType': { event_type } });
return (
);
@@ -168,13 +89,9 @@ export default class DevicesPanelEntry extends React.Component {
DevicesPanelEntry.propTypes = {
device: React.PropTypes.object.isRequired,
- onDeleted: React.PropTypes.func,
-};
-
-DevicesPanelEntry.contextTypes = {
- authCache: React.PropTypes.object,
+ onDeviceToggled: React.PropTypes.func,
};
DevicesPanelEntry.defaultProps = {
- onDeleted: function() {},
+ onDeviceToggled: function() {},
};
diff --git a/src/components/views/voip/VideoFeed.js b/src/components/views/voip/VideoFeed.js
index 953dbc866f..f955df62d9 100644
--- a/src/components/views/voip/VideoFeed.js
+++ b/src/components/views/voip/VideoFeed.js
@@ -39,7 +39,7 @@ module.exports = React.createClass({
},
onResize: function(e) {
- if(this.props.onResize) {
+ if (this.props.onResize) {
this.props.onResize(e);
}
},
diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js
index 748673f1a5..44e7a47f02 100644
--- a/src/components/views/voip/VideoView.js
+++ b/src/components/views/voip/VideoView.js
@@ -23,7 +23,7 @@ import classNames from 'classnames';
import sdk from '../../../index';
import dis from '../../../dispatcher';
-import UserSettingsStore from '../../../UserSettingsStore';
+import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({
displayName: 'VideoView',
@@ -113,7 +113,7 @@ module.exports = React.createClass({
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
{ "mx_VideoView_localVideoFeed_flipped":
- UserSettingsStore.getSyncedSetting('VideoView.flipVideoHorizontally', false),
+ SettingsStore.getValue('VideoView.flipVideoHorizontally'),
},
);
return (
diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index 91a71937d4..c797044693 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -44,15 +44,15 @@
"Create new room": "Založit novou místnost",
"Room directory": "Adresář místností",
"Start chat": "Začít chat",
- "Options": "Možnosti",
+ "Options": "Volby",
"Register": "Zaregistrovat",
"Cancel": "Storno",
"Error": "Chyba",
"Favourite": "V oblíbených",
"Mute": "Ztlumit",
"Continue": "Pokračovat",
- "Failed to change password. Is your password correct?": "Nepodařilo se změnit heslo. Je vaše heslo správné?",
- "Operation failed": "Chyba operace",
+ "Failed to change password. Is your password correct?": "Nepodařilo se změnit heslo. Zadáváte své heslo správně?",
+ "Operation failed": "Operace se nezdařila",
"Remove": "Odebrat",
"unknown error code": "neznámý kód chyby",
"OK": "OK",
@@ -140,7 +140,7 @@
"Decline": "Odmítnout",
"Decrypt %(text)s": "Dešifrovat %(text)s",
"Decryption error": "Chyba dešifrování",
- "Delete": "Vymazat",
+ "Delete": "Smazat",
"Delete widget": "Vymazat widget",
"Default": "Výchozí",
"Device already verified!": "Zařízení již bylo ověřeno!",
@@ -188,22 +188,22 @@
"Failed to delete device": "Nepodařilo se vymazat zařízení",
"Failed to join room": "Vstup do místnosti se nezdařil",
"Failed to kick": "Vykopnutí se nezdařilo",
- "Failed to leave room": "Opuštění místnosti se nezdařilo",
+ "Failed to leave room": "Odejití z místnosti se nezdařilo",
"Failed to mute user": "Ztlumení uživatele se nezdařilo",
"Failed to send email": "Odeslání e-mailu se nezdařilo",
- "Failed to save settings": "Uložení nastavení se nezdařilo",
- "Failed to reject invitation": "Odmítnutí pozvánky se nezdařilo",
- "Failed to reject invite": "Odmítnutí pozvání se nezdařilo",
+ "Failed to save settings": "Nepodařilo se uložit nastavení",
+ "Failed to reject invitation": "Nepodařilo se odmítnout pozvání",
+ "Failed to reject invite": "Nepodařilo se odmítnout pozvánku",
"Failed to send request.": "Odeslání žádosti se nezdařilo.",
"Failed to set avatar.": "Nastavení avataru se nezdařilo.",
- "Failed to set display name": "Nastavení zobrazovaného jména se nezdařilo",
- "Failed to set up conference call": "Nastavení konferenčního hovoru se nezdařilo",
+ "Failed to set display name": "Nepodařilo se nastavit zobrazované jméno",
+ "Failed to set up conference call": "Nepodařilo se nastavit konferenční hovor",
"Failed to toggle moderator status": "Změna statusu moderátora se nezdařila",
- "Failed to unban": "Odvolání vykázání se nezdařilo",
+ "Failed to unban": "Přijetí zpět se nezdařilo",
"Failed to upload profile picture!": "Nahrání profilového obrázku se nezdařilo",
"Failure to create room": "Vytvoření místnosti se nezdařilo",
"Forget room": "Zapomenout místnost",
- "Forgot your password?": "Zapomněli jste své heslo?",
+ "Forgot your password?": "Zapomněl/a jste své heslo?",
"For security, this session has been signed out. Please sign in again.": "Z bezpečnostních důvodů bylo toto přihlášení ukončeno. Přihlašte se prosím znovu.",
"%(names)s and one other are typing": "%(names)s a jeden další píší",
"%(names)s and %(lastPerson)s are typing": "%(names)s a %(lastPerson)s píší",
@@ -307,10 +307,10 @@
"Send anyway": "Přesto poslat",
"Sender device information": "Informace o odesilatelově zařízení",
"Send Reset Email": "Poslat resetovací e-mail",
- "sent an image": "poslat obrázek",
+ "sent an image": "poslal/a obrázek",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s poslal/a obrázek.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s poslal/a %(targetDisplayName)s pozvánku ke vstupu do místnosti.",
- "sent a video": "poslat video",
+ "sent a video": "poslal/a video",
"Server error": "Chyba serveru",
"Server may be unavailable or overloaded": "Server může být nedostupný nebo přetížený",
"Server may be unavailable, overloaded, or search timed out :(": "Server může být nedostupný, přetížený nebo vyhledávání vypršelo :(",
@@ -371,48 +371,48 @@
"Start chatting": "Začít chatovat",
"Start Chatting": "Začít chatovat",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Textová zpráva byla odeslána na +%(msisdn)s. Prosím vložte ověřovací kód z dané zprávy",
- "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s přijmul/a pozvánku pro %(displayName)s.",
+ "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s přijal/a pozvánku pro %(displayName)s.",
"Active call (%(roomName)s)": "Probíhající hovor (%(roomName)s)",
- "An email has been sent to": "Email byl odeslán odeslán na",
- "%(senderName)s banned %(targetName)s.": "%(senderName)s zablokoval/a %(targetName)s.",
- "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Nelze se připojit k homeserveru přes HTTP pokud je v adresním řádku HTTPS. Buď použijte HTTPS nebo povolte nebezpečné scripty.",
- "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.": "%(senderName)s změnil/a zobrazované jméno z %(oldDisplayName)s na %(displayName)s.",
+ "An email has been sent to": "E-mail byl odeslán odeslán na",
+ "%(senderName)s banned %(targetName)s.": "%(senderName)s vykázal/a %(targetName)s.",
+ "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Nelze se připojit k domovskému serveru přes HTTP, pokud je v adresním řádku HTTPS. Buď použijte HTTPS, nebo povolte nebezpečné scripty.",
+ "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.": "%(senderName)s změnil/a své zobrazované jméno z %(oldDisplayName)s na %(displayName)s.",
"Click here to fix": "Klikněte zde pro opravu",
"Click to mute video": "Klikněte pro zakázání videa",
"click to reveal": "klikněte pro odhalení",
"Click to unmute video": "Klikněte pro povolení videa",
"Click to unmute audio": "Klikněte pro povolení zvuku",
- "Devices will not yet be able to decrypt history from before they joined the room": "Zařízení nebudou schopna dešifrovat historii před tím než se připojila k místnosti",
+ "Devices will not yet be able to decrypt history from before they joined the room": "Zařízení nebudou schopna dešifrovat historii z doby před jejich vstupem do místnosti",
"Displays action": "Zobrazí akci",
"Do you want to load widget from URL:": "Chcete načíst widget z URL:",
"Ed25519 fingerprint": "Ed25519 otisk",
"Fill screen": "Vyplnit obrazovku",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s z %(fromPowerLevel)s na %(toPowerLevel)s",
- "This doesn't appear to be a valid email address": "Emailová adresa se zdá být nevalidní",
+ "This doesn't appear to be a valid email address": "Tato e-mailová adresa se zdá být neplatná",
"This is a preview of this room. Room interactions have been disabled": "Toto je náhled místnosti. Interakce byly zakázány",
- "This phone number is already in use": "Tohle číslo už se používá",
- "This room is not accessible by remote Matrix servers": "Tahle místnost není přístupná vzdálenými Matrix servery",
- "This room's internal ID is": "Vnitřní ID místnosti je",
- "To reset your password, enter the email address linked to your account": "K resetování hesla, vložte emailovou adresu spojenou s vaším účtem",
- "to restore": "obnovit",
- "to tag direct chat": "oštítkovat přímý chat",
- "To use it, just wait for autocomplete results to load and tab through them.": "Pro použití vyčkejte k načtení automatického doplňování a tabem přeskakujte mezi výsledky.",
+ "This phone number is already in use": "Toto číslo se již používá",
+ "This room is not accessible by remote Matrix servers": "Tato místnost není přístupná vzdáleným Matrix serverům",
+ "This room's internal ID is": "Vnitřní ID této místnosti je",
+ "To reset your password, enter the email address linked to your account": "K resetování hesla vložte e-mailovou adresu spojenou s vaším účtem",
+ "to restore": "obnovíte",
+ "to tag direct chat": "oštítkujete přímý chat",
+ "To use it, just wait for autocomplete results to load and tab through them.": "Použijte tak, že vyčkáte na načtení našeptávaných výsledků a ty pak projdete tabulátorem.",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Nemáte práva k zobrazení zprávy v daném časovém úseku.",
- "Tried to load a specific point in this room's timeline, but was unable to find it.": "Zpráva v daném časovém úsaku nenalezena.",
+ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Zpráva v daném časovém úseku nenalezena.",
"Turn Markdown off": "Vypnout Markdown",
"Turn Markdown on": "Zapnout Markdown",
- "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s zapnul end-to-end šifrování (algoritmus %(algorithm)s).",
- "Unable to add email address": "Nepodařilo se přidat emailovou adresu",
+ "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s zapnul/a end-to-end šifrování (algoritmus %(algorithm)s).",
+ "Unable to add email address": "Nepodařilo se přidat e-mailovou adresu",
"Unable to create widget.": "Nepodařilo se vytvořit widget.",
"Unable to remove contact information": "Nepodařilo se smazat kontaktní údaje",
- "Unable to verify email address.": "Nepodařilo se ověřit emailovou adresu.",
- "Unban": "Odblokovat",
- "Unbans user with given id": "Odblokuje uživatele s daným id",
- "%(senderName)s unbanned %(targetName)s.": "%(senderName)s odblokoval/a %(targetName)s.",
- "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Nepodařilo se prokázat že adresa na kterou byla tato pozvánka odeslána se shoduje s adresou přiřazenou Vašemu účtu.",
+ "Unable to verify email address.": "Nepodařilo se ověřit e-mailovou adresu.",
+ "Unban": "Přijmout zpět",
+ "Unbans user with given id": "Přijme zpět uživatele s daným id",
+ "%(senderName)s unbanned %(targetName)s.": "%(senderName)s přijal/a zpět %(targetName)s.",
+ "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Nepodařilo se prokázat, že adresa, na kterou byla tato pozvánka odeslána, se shoduje s adresou přidruženou k vašemu účtu.",
"Unable to capture screen": "Nepodařilo se zachytit obrazovku",
- "Unable to enable Notifications": "Nepodařilo se povolit Notifikace",
- "Unable to load device list": "Nepodařilo se načíst list zařízení",
+ "Unable to enable Notifications": "Nepodařilo se povolit upozornění",
+ "Unable to load device list": "Nepodařilo se načíst seznam zařízení",
"Undecryptable": "Nerozšifrovatelné",
"unencrypted": "nešifrované",
"Unencrypted message": "Nešifrovaná zpráva",
@@ -421,15 +421,15 @@
"Unknown room %(roomId)s": "Neznámá místnost %(roomId)s",
"Unknown (user, device) pair:": "Neznámý pár (uživatel, zařízení):",
"Unmute": "Povolit",
- "Unnamed Room": "Nepojmenovaná Místnost",
+ "Unnamed Room": "Nepojmenovaná místnost",
"Unrecognised command:": "Nerozpoznaný příkaz:",
"Unrecognised room alias:": "Nerozpoznaný alias místnosti:",
"Unverified": "Neověřený",
"Uploading %(filename)s and %(count)s others|zero": "Nahrávám %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Nahrávám %(filename)s a %(count)s další",
"Uploading %(filename)s and %(count)s others|other": "Nahrávám %(filename)s a %(count)s další",
- "Upload Failed": "Nahrávání Selhalo",
- "Upload Files": "Nahrát Soubory",
+ "Upload Failed": "Nahrávání selhalo",
+ "Upload Files": "Nahrát soubory",
"Upload file": "Nahrát soubor",
"Upload new:": "Nahrát nový:",
"Usage": "Použití",
@@ -439,16 +439,238 @@
"User Interface": "Uživatelské rozhraní",
"%(user)s is a": "%(user)s je",
"User name": "Uživatelské jméno",
- "Username invalid: %(errMessage)s": "Nevalidní uživatelské jméno: %(errMessage)s",
+ "Username invalid: %(errMessage)s": "Neplatné uživatelské jméno: %(errMessage)s",
"Users": "Uživatelé",
"User": "Uživatel",
"Verification Pending": "Čeká na ověření",
"Verification": "Ověření",
"verified": "ověreno",
"Verified": "Ověřeno",
- "Verified key": "Ověřen klíč",
+ "Verified key": "Ověřený klíč",
"(no answer)": "(žádná odpověď)",
"(unknown failure: %(reason)s)": "(neznámá chyba: %(reason)s)",
- "(warning: cannot be disabled again!)": "(varování: nemůže být opět zakázáno!)",
- "WARNING: Device already verified, but keys do NOT MATCH!": "VAROVÁNÍ: Zařízení ověřeno, ale klíče se NESHODUJÍ!"
+ "(warning: cannot be disabled again!)": "(varování: nepůjde znovu zakázat!)",
+ "WARNING: Device already verified, but keys do NOT MATCH!": "VAROVÁNÍ: Zařízení byl již ověřeno, ale klíče se NESHODUJÍ!",
+ "The remote side failed to pick up": "Vzdálené straně se nepodařilo hovor přijmout",
+ "Who would you like to add to this community?": "Koho chcete přidat do této komunity?",
+ "Invite new community members": "Pozvěte nové členy komunity",
+ "Name or matrix ID": "Jméno nebo matrix ID",
+ "Invite to Community": "Pozvat do komunity",
+ "Which rooms would you like to add to this community?": "Které místnosti chcete přidat do této komunity?",
+ "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID": "Varování: místnost, kterou přidáte do této komunity, bude veřejně viditelná každému, kdo zná ID komunity",
+ "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Varování: osoba, kterou přidáte do této komunity, bude veřejně viditelná každému, kdo zná ID komunity",
+ "Add rooms to the community": "Přidat místnosti do komunity",
+ "Room name or alias": "Název nebo alias místnosti",
+ "Add to community": "Přidat do komunity",
+ "Failed to invite the following users to %(groupId)s:": "Následující uživatele se nepodařilo přidat do %(groupId)s:",
+ "Invites sent": "Pozvánky odeslány",
+ "Your community invitations have been sent.": "Vaše komunitní pozvánky byly odeslány.",
+ "Failed to invite users to community": "Nepodařilo se pozvat uživatele do komunity",
+ "Failed to invite users to %(groupId)s": "Nepodařilo se pozvat uživatele do %(groupId)s",
+ "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
+ "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
+ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
+ "Failed to add the following rooms to %(groupId)s:": "Nepodařilo se přidat následující místnosti do %(groupId)s:",
+ "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Vaše e-mailová adresa zřejmě nepatří k žádnému Matrix ID na tomto domovském serveru.",
+ "Send Invites": "Odeslat pozvánky",
+ "Failed to invite user": "Nepodařilo se pozvat uživatele",
+ "Failed to invite": "Pozvání se nezdařilo",
+ "Failed to invite the following users to the %(roomName)s room:": "Do místnosti %(roomName)s se nepodařilo pozvat následující uživatele:",
+ "You need to be logged in.": "Musíte být přihlášen/a.",
+ "You are now ignoring %(userId)s": "Nyní ignorujete %(userId)s",
+ "You are no longer ignoring %(userId)s": "Už neignorujete %(userId)s",
+ "Add rooms to this community": "Přidat místnosti do této komunity",
+ "Unpin Message": "Odepnout zprávu",
+ "Ignored user": "Ignorovaný uživatel",
+ "Unignored user": "Odignorovaný uživatel",
+ "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "VAROVÁNÍ: OVĚŘENÍ KLÍČE SELHALO! Podepisovací klíč uživatele %(userId)s a zařízení %(deviceId)s je \"%(fprint)s\", což nesouhlasí s dodaným klíčem \"%(fingerprint)s\". Toto může znamenat, že vaše komunikace je odposlouchávána!",
+ "Reason": "Důvod",
+ "VoIP conference started.": "VoIP konference započata.",
+ "VoIP conference finished.": "VoIP konference ukončena.",
+ "%(targetName)s left the room.": "%(targetName)s opustil/a místnost.",
+ "You are already in a call.": "Již máte probíhající hovor.",
+ "%(senderName)s requested a VoIP conference.": "%(senderName)s požádal/a o VoIP konferenci.",
+ "%(senderName)s removed their profile picture.": "%(senderName)s odstranil/a svůj profilový obrázek.",
+ "%(targetName)s rejected the invitation.": "%(targetName)s odmítl/a pozvání.",
+ "Communities": "Komunity",
+ "Message Pinning": "Připíchnutí zprávy",
+ "Your browser does not support the required cryptography extensions": "Váš prohlížeč nepodporuje požadovaná kryptografická rozšíření",
+ "Do you want to set an email address?": "Chcete nastavit e-mailovou adresu?",
+ "New Password": "Nové heslo",
+ "Device Name": "Název zařízení",
+ "Unignore": "Odignorovat",
+ "Ignore": "Ignorovat",
+ "Admin Tools": "Nástroje pro správce",
+ "bold": "tučně",
+ "italic": "kurzíva",
+ "strike": "přeškrtnutí",
+ "underline": "podtržení",
+ "code": "kód",
+ "quote": "citace",
+ "bullet": "odrážka",
+ "numbullet": "číselný seznam",
+ "No pinned messages.": "Žádné připíchnuté zprávy.",
+ "Pinned Messages": "Připíchnuté zprávy",
+ "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s odstranil/a svoje zobrazované jméno (%(oldDisplayName)s).",
+ "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s odvolal/a pozvánku pro %(targetName)s.",
+ "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s učinil/a budoucí historii místnosti viditelnou všem členům, a to od chvíle jejich pozvání.",
+ "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s učinil/a budoucí historii místnosti viditelnou všem členům, a to od chvíle jejich vstupu do místnosti.",
+ "%(senderName)s made future room history visible to all room members.": "%(senderName)s učinil/a budoucí historii místnosti viditelnou všem členům.",
+ "%(senderName)s made future room history visible to anyone.": "%(senderName)s učinil/a budoucí historii místnosti viditelnou komukoliv.",
+ "%(senderName)s changed the pinned messages for the room.": "%(senderName)s změnil/a připíchnuté zprávy této místnosti.",
+ "%(names)s and %(count)s others are typing|other": "%(names)s a %(count)s další píší",
+ "Authentication check failed: incorrect password?": "Kontrola ověření selhala: špatné heslo?",
+ "You need to be able to invite users to do that.": "Pro tuto akci musíte mít právo zvát uživatele.",
+ "Delete Widget": "Smazat widget",
+ "Error decrypting image": "Chyba při dešifrování obrázku",
+ "Image '%(Body)s' cannot be displayed.": "Obrázek '%(Body)s' nemůže být zobrazen.",
+ "This image cannot be displayed.": "Tento obrázek nemůže být zobrazen.",
+ "Error decrypting video": "Chyba při dešifrování videa",
+ "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s odstranil/a avatar místnosti.",
+ "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s změnil/a avatar místnosti na ",
+ "Copied!": "Zkopírováno!",
+ "Failed to copy": "Nepodařilo se zkopírovat",
+ "Removed or unknown message type": "Zpráva odstraněna nebo neznámého typu",
+ "Message removed by %(userId)s": "Zprávu odstranil/a %(userId)s",
+ "This Home Server would like to make sure you are not a robot": "Tento domovský server by se rád přesvědčil, že nejste robot",
+ "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "Přes vlastní serverové volby se můžete přihlásit k dalším Matrix serverům tak, že zadáte jinou adresu domovského serveru.",
+ "Identity server URL": "Adresa serveru identity",
+ "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "Taktéž můžete zadat vlastní server identity, ale to vám zpravidla znemožní interagovat s uživateli na základě e-mailové adresy.",
+ "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Smazáním widgetu jej odstraníte všem uživatelům v této místnosti. Určitě chcete tento widget smazat?",
+ "The maximum permitted number of widgets have already been added to this room.": "V této místnosti již bylo dosaženo limitu pro maximální počet widgetů.",
+ "Drop file here to upload": "Přetažením sem nahrajete",
+ "uploaded a file": "nahrál/a soubor",
+ "Example": "Příklad",
+ "Create Community": "Vytvořit komunitu",
+ "Community Name": "Název komunity",
+ "Community ID": "ID komunity",
+ "example": "příklad",
+ "Create": "Vytvořit",
+ "Advanced options": "Pokročilé volby",
+ "User Options": "Volby uživatele",
+ "Please select the destination room for this message": "Vyberte prosím pro tuto zprávu cílovou místnost",
+ "No devices with registered encryption keys": "Žádná zařízení se zaregistrovanými šifrovacími klíči",
+ "Jump to read receipt": "Přeskočit na potvrzení o přečtení",
+ "Invite": "Pozvat",
+ "Revoke Moderator": "Odebrat moderátorství",
+ "Make Moderator": "Udělit moderátorství",
+ "and %(count)s others...|one": "a někdo další...",
+ "Hangup": "Zavěsit",
+ "Hide Apps": "Skrýt aplikace",
+ "Show Text Formatting Toolbar": "Zobrazit nástroje formátování textu",
+ "Hide Text Formatting Toolbar": "Skrýt nástroje formátování textu",
+ "Jump to message": "Přeskočit na zprávu",
+ "Loading...": "Načítání...",
+ "Loading device info...": "Načítá se info o zařízení...",
+ "You seem to be uploading files, are you sure you want to quit?": "Zřejmě právě nahráváte soubory. Chcete přesto odejít?",
+ "You seem to be in a call, are you sure you want to quit?": "Zřejmě máte probíhající hovor. Chcete přesto odejít?",
+ "Idle": "Nečinný/á",
+ "Unknown": "Neznámý",
+ "Seen by %(userName)s at %(dateTime)s": "Spatřen uživatelem %(userName)s v %(dateTime)s",
+ "Unnamed room": "Nepojmenovaná místnost",
+ "World readable": "Světu čitelné",
+ "Guests can join": "Hosté mohou vstoupit",
+ "No rooms to show": "Žádné místnosti k zobrazení",
+ "(~%(count)s results)|other": "(~%(count)s výsledků)",
+ "(~%(count)s results)|one": "(~%(count)s výsledek)",
+ "Upload avatar": "Nahrát avatar",
+ "Remove avatar": "Odstranit avatar",
+ "Mention": "Zmínka",
+ "Blacklisted": "Na černé listině",
+ "Invited": "Pozvaní",
+ "Markdown is disabled": "Markdown je vypnutý",
+ "Markdown is enabled": "Markdown je zapnutý",
+ "Press to start a chat with someone": "Zmáčkněte a můžete začít chatovat",
+ "This invitation was sent to an email address which is not associated with this account:": "Tato pozvánka byla odeslána na e-mailovou aresu, která není přidružená k tomuto účtu:",
+ "Joins room with given alias": "Vstoupí do místnosti s daným aliasem",
+ "were unbanned": "byli přijati zpět",
+ "was unbanned": "byl/a přijat/a zpět",
+ "was unbanned %(repeats)s times": "byl/a přijat/a zpět %(repeats)skrát",
+ "were unbanned %(repeats)s times": "byli přijati zpět %(repeats)skrát",
+ "Leave Community": "Odejít z komunity",
+ "Leave %(groupName)s?": "Odejít z %(groupName)s?",
+ "Leave": "Odejít",
+ "Unable to leave room": "Nepodařilo se odejít z místnosti",
+ "Hide join/leave messages (invites/kicks/bans unaffected)": "Skrýt zprávy o vstupu či odejití (pozvánky, vykopnutí a vykázání zůstanou)",
+ "%(severalUsers)sjoined and left %(repeats)s times": "%(severalUsers)s vstoupilo a odešlo %(repeats)skrát",
+ "%(oneUser)sjoined and left %(repeats)s times": "%(oneUser)s vstoupil/a a odešel/la %(repeats)skrát",
+ "%(severalUsers)sjoined and left": "%(severalUsers)s vstoupilo a odešlo",
+ "%(oneUser)sjoined and left": "%(oneUser)s vstoupil/a a odešel/la",
+ "Failed to remove user from community": "Nepodařilo se odebrat uživatele z komunity",
+ "Failed to remove room from community": "Nepodařilo se odebrat místnost z komunity",
+ "Failed to remove '%(roomName)s' from %(groupId)s": "'%(roomName)s' se nepodařilo odebrat z %(groupId)s",
+ "Failed to update community": "Nepodařilo se aktualizovat komunitu",
+ "Failed to load %(groupId)s": "Nepodařilo se načíst %(groupId)s",
+ "Search failed": "Vyhledávání selhalo",
+ "Failed to fetch avatar URL": "Nepodařilo se získat adresu avataru",
+ "Error decrypting audio": "Chyba při dešifrování zvuku",
+ "Drop here to tag %(section)s": "Přetažením sem oštítkujete %(section)s",
+ "You have been invited to join this room by %(inviterName)s": "%(inviterName)s vás pozval/a ke vstupu do této místnosti",
+ "Reason: %(reasonText)s": "Důvod: %(reasonText)s",
+ "Rejoin": "Vstoupit znovu",
+ "To change the room's avatar, you must be a": "Abyste mohl/a měnit avatar místnosti, musíte být",
+ "To change the room's name, you must be a": "Abyste mohl/a měnit název místnosti, musíte být",
+ "To change the room's main address, you must be a": "Abyste mohl/a měnit hlavní adresu místnosti, musíte být",
+ "To change the room's history visibility, you must be a": "Abyste mohl/a měnit viditelnost historie místnosti, musíte být",
+ "To change the permissions in the room, you must be a": "Abyste mohl/a měnit oprávnění v místnosti, musíte být",
+ "To change the topic, you must be a": "Abyste mohl/a měnit téma, musíte být",
+ "To modify widgets in the room, you must be a": "Abyste mohl/a měnit widgety v místnosti, musíte být",
+ "Banned by %(displayName)s": "Vykázán/a uživatelem %(displayName)s",
+ "Privacy warning": "Výstraha o utajení",
+ "Never send encrypted messages to unverified devices in this room from this device": "Nikdy z tohoto zařízení neposílat šifrované zprávy neověřeným zařízením v této místnosti",
+ "Privileged Users": "Privilegovaní uživatelé",
+ "No users have specific privileges in this room": "Žádní uživatelé v této místnosti nemají zvláštní privilegia",
+ "Tagged as: ": "Oštítkováno jako: ",
+ "To link to a room it must have an address.": "Aby šlo odkazovat na místnost, musí mít adresu.",
+ "Publish this room to the public in %(domain)s's room directory?": "Zapsat tuto místnost do veřejného adresáře místností na %(domain)s?",
+ "since the point in time of selecting this option": "od chvíle aktivování této volby",
+ "since they were invited": "od chvíle jejich pozvání",
+ "since they joined": "od chvíle jejich vstupu",
+ "The default role for new room members is": "Výchozí role nových členů místnosti je",
+ "To send messages, you must be a": "Abyste mohl/a posílat zprávy, musíte být",
+ "To invite users into the room, you must be a": "Abyste mohl/a zvát uživatele do této místnosti, musíte být",
+ "To configure the room, you must be a": "Abyste mohl/a nastavovat tuto místnost, musíte být",
+ "To kick users, you must be a": "Abyste mohl/a vykopávat uživatele, musíte být",
+ "To ban users, you must be a": "Abyste mohl/a vykazovat uživatele, musíte být",
+ "To remove other users' messages, you must be a": "Abyste mohl/a odstraňovat zprávy ostatních uživatelů, musíte být",
+ "To send events of type , you must be a": "Abyste mohl/a odesílat události typu , musíte být",
+ "You should not yet trust it to secure data": "Zatím byste jeho zabezpečení dat neměl/a důvěřovat",
+ "Remote addresses for this room:": "Vzdálené adresy této místnosti:",
+ "Invalid community ID": "Neplatné ID komunity",
+ "'%(groupId)s' is not a valid community ID": "'%(groupId)s' není platné ID komunity",
+ "Related Communities": "Související komunity",
+ "Related communities for this room:": "Komunity související s touto místností:",
+ "This room has no related communities": "Tato místnost nemá žádné související komunity",
+ "New community ID (e.g. +foo:%(localDomain)s)": "Nové ID komunity (např. +neco:%(localDomain)s)",
+ "%(names)s and %(count)s others are typing|one": "%(names)s a jeden další píší",
+ "%(senderName)s sent an image": "%(senderName)s poslal/a obrázek",
+ "%(senderName)s sent a video": "%(senderName)s poslal/a video",
+ "%(senderName)s uploaded a file": "%(senderName)s nahrál/a soubor",
+ "Disinvite this user?": "Odvolat pozvání tohoto uživatele?",
+ "Kick this user?": "Vykopnout tohoto uživatele?",
+ "Unban this user?": "Přijmout zpět tohoto uživatele?",
+ "Ban this user?": "Vykázat tohoto uživatele?",
+ "Drop here to favourite": "Oblibte přetažením zde",
+ "Drop here to tag direct chat": "Přímý chat oštítkujte přetažením zde",
+ "Drop here to restore": "Obnovte přetažením zde",
+ "Drop here to demote": "Upozaďte přetažením zde",
+ "Community Invites": "Komunitní pozvánky",
+ "You have been kicked from this room by %(userName)s.": "%(userName)s vás vykopl/a z této místnosti.",
+ "You have been banned from this room by %(userName)s.": "%(userName)s vás vykázal/a z této místnosti.",
+ "You are trying to access a room.": "Pokoušíte se o přístup do místnosti.",
+ "Members only (since the point in time of selecting this option)": "Pouze členové (od chvíle vybrání této volby)",
+ "Members only (since they were invited)": "Pouze členové (od chvíle jejich pozvání)",
+ "Members only (since they joined)": "Pouze členové (od chvíle jejich vstupu)",
+ "Disable URL previews by default for participants in this room": "Vypnout účastníkům v této místnosti automatické náhledy webových adres",
+ "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.": "Automatické zobrazení náhledů webových adres je v této místnosti pro všechny účastníky %(globalDisableUrlPreview)s.",
+ "You have disabled URL previews by default.": "Vypnul/a jste automatické náhledy webových adres.",
+ "You have enabled URL previews by default.": "Zapnul/a jste automatické náhledy webových adres.",
+ "URL Previews": "Náhledy webových adres",
+ "Enable URL previews for this room (affects only you)": "Zapnout náhledy webových adres (pouze vám)",
+ "Disable URL previews for this room (affects only you)": "Vypnout náhledy webových adres (pouze vám)",
+ "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s změnil/a avatar místnosti %(roomName)s",
+ "Add an Integration": "Přidat začlenění",
+ "Message removed": "Zpráva odstraněna",
+ "Robot check is currently unavailable on desktop - please use a web browser": "Ochrana před roboty není aktuálně na desktopu dostupná. Použijte prosím webový prohlížeč",
+ "An email has been sent to %(emailAddress)s": "Na adresu %(emailAddress)s jsme poslali e-mail"
}
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 8ff76a239e..db12a69657 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -21,10 +21,10 @@
"User ID": "Benutzer-ID",
"Curve25519 identity key": "Curve25519-Identitäts-Schlüssel",
"Claimed Ed25519 fingerprint key": "Geforderter Ed25519-Fingerprint-Schlüssel",
- "none": "keiner",
+ "none": "nicht vorhanden",
"Algorithm": "Algorithmus",
"unencrypted": "unverschlüsselt",
- "Decryption error": "Entschlüsselungs Fehler",
+ "Decryption error": "Fehler beim Entschlüsseln",
"Session ID": "Sitzungs-ID",
"End-to-end encryption information": "Informationen zur Ende-zu-Ende-Verschlüsselung",
"Event information": "Ereignis-Information",
@@ -76,7 +76,7 @@
"End-to-end encryption is in beta and may not be reliable": "Die Ende-zu-Ende-Verschlüsselung befindet sich aktuell im Beta-Stadium und ist eventuell noch nicht hundertprozentig zuverlässig",
"Failed to send email": "Fehler beim Senden der E-Mail",
"Account": "Benutzerkonto",
- "Add phone number": "Telefonnummer hinzufügen",
+ "Add phone number": "Telefon-Nr. hinzufügen",
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du wirst erst Benachrichtigungen auf anderen Geräten empfangen können, wenn du dich dort erneut anmeldest",
"Can't load user settings": "Benutzereinstellungen können nicht geladen werden",
"Clear Cache": "Cache leeren",
@@ -138,7 +138,7 @@
"Scroll to unread messages": "Zu den ungelesenen Nachrichten scrollen",
"Send Invites": "Einladungen senden",
"Send Reset Email": "E-Mail zum Zurücksetzen senden",
- "sent an image": "hat ein Bild gesendet",
+ "sent an image": "hat ein Bild übermittelt",
"sent a video": "hat ein Video gesendet",
"Server may be unavailable or overloaded": "Server ist eventuell nicht verfügbar oder überlastet",
"Settings": "Einstellungen",
@@ -158,7 +158,7 @@
"This room is not accessible by remote Matrix servers": "Remote-Matrix-Server können auf diesen Raum nicht zugreifen",
"This room's internal ID is": "Die interne ID dieses Raumes ist",
"Admin": "Administrator",
- "Server may be unavailable, overloaded, or you hit a bug.": "Server ist nicht verfügbar, überlastet oder du bist auf einen Fehler gestoßen.",
+ "Server may be unavailable, overloaded, or you hit a bug.": "Server ist nicht verfügbar, überlastet oder du bist auf einen Softwarefehler gestoßen.",
"Could not connect to the integration server": "Konnte keine Verbindung zum Integrations-Server herstellen",
"Disable inline URL previews by default": "URL-Vorschau im Chat standardmäßig deaktivieren",
"Labs": "Labor",
@@ -262,7 +262,7 @@
"Encrypt room": "Raum verschlüsseln",
"%(names)s and %(lastPerson)s are typing": "%(names)s und %(lastPerson)s schreiben",
"%(targetName)s accepted an invitation.": "%(targetName)s hat eine Einladung angenommen.",
- "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s akzeptierte die Einladung für %(displayName)s.",
+ "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s hat die Einladung für %(displayName)s akzeptiert.",
"%(names)s and one other are typing": "%(names)s und ein weiteres Raum-Mitglied schreiben",
"%(senderName)s answered the call.": "%(senderName)s hat den Anruf angenommen.",
"%(senderName)s banned %(targetName)s.": "%(senderName)s hat %(targetName)s dauerhaft aus dem Raum verbannt.",
@@ -284,7 +284,7 @@
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für alle Raum-Mitglieder (ab dem Zeitpunkt, an dem sie eingeladen wurden).",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für alle Raum-Mitglieder (ab dem Zeitpunkt, an dem sie beigetreten sind).",
"%(senderName)s made future room history visible to all room members.": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für alle Raum-Mitglieder.",
- "%(senderName)s made future room history visible to anyone.": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für Jeder.",
+ "%(senderName)s made future room history visible to anyone.": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für Alle.",
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für unbekannt (%(visibility)s).",
"Missing room_id in request": "Fehlende room_id in Anfrage",
"Missing user_id in request": "Fehlende user_id in Anfrage",
@@ -298,7 +298,7 @@
"%(senderName)s removed their profile picture.": "%(senderName)s hat das Profilbild gelöscht.",
"%(senderName)s requested a VoIP conference.": "%(senderName)s möchte eine VoIP-Konferenz beginnen.",
"Room %(roomId)s not visible": "Raum %(roomId)s ist nicht sichtbar",
- "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s hat ein Bild gesendet.",
+ "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s hat ein Bild übermittelt.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s hat %(targetDisplayName)s in diesen Raum eingeladen.",
"%(senderName)s set a profile picture.": "%(senderName)s hat ein Profilbild gesetzt.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s hat den Anzeigenamen geändert in %(displayName)s.",
@@ -364,7 +364,7 @@
"Markdown is disabled": "Markdown ist deaktiviert",
"Markdown is enabled": "Markdown ist aktiviert",
"Message not sent due to unknown devices being present": "Nachrichten wurden nicht gesendet, da unbekannte Geräte anwesend sind",
- "New address (e.g. #foo:%(localDomain)s)": "Neue Adresse (z.B. #foo:%(localDomain)s)",
+ "New address (e.g. #foo:%(localDomain)s)": "Neue Adresse (z. B. #foo:%(localDomain)s)",
"not set": "nicht gesetzt",
"not specified": "nicht spezifiziert",
"No devices with registered encryption keys": "Keine Geräte mit registrierten Verschlüsselungs-Schlüsseln",
@@ -403,7 +403,7 @@
"bullet": "Aufzählung",
"Click to unmute video": "Klicken, um die Video-Stummschaltung zu deaktivieren",
"Click to unmute audio": "Klicken, um den Ton wieder einzuschalten",
- "Failed to load timeline position": "Laden der Position im Zeitstrahl fehlgeschlagen",
+ "Failed to load timeline position": "Laden der Position im Chatverlauf fehlgeschlagen",
"Failed to toggle moderator status": "Umschalten des Moderator-Status fehlgeschlagen",
"Enable encryption": "Verschlüsselung aktivieren",
"The main address for this room is": "Die Hauptadresse für diesen Raum ist",
@@ -450,7 +450,7 @@
"was kicked %(repeats)s times": "wurde %(repeats)s-mal gekickt",
"were kicked": "wurden gekickt",
"%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)shaben ihren Namen %(repeats)s mal geändert",
- "%(oneUser)schanged their name %(repeats)s times": "%(oneUser)shat den Namen %(repeats)s mal geändert",
+ "%(oneUser)schanged their name %(repeats)s times": "%(oneUser)shat den Namen %(repeats)s-mal geändert",
"%(severalUsers)schanged their name": "%(severalUsers)shaben ihre Namen geändert",
"%(oneUser)schanged their name": "%(oneUser)shat den Namen geändert",
"%(severalUsers)schanged their avatar %(repeats)s times": "%(severalUsers)shaben %(repeats)s mal ihr Profilbild geändert",
@@ -596,7 +596,7 @@
"Camera": "Kamera",
"Device already verified!": "Gerät bereits verifiziert!",
"Export": "Export",
- "Guest access is disabled on this Home Server.": "Gastzugang ist auf diesem Heimserver deaktivert.",
+ "Guest access is disabled on this Home Server.": "Der Gastzugang ist auf diesem Heimserver deaktiviert.",
"Import": "Importieren",
"Incorrect username and/or password.": "Inkorrekter Nutzername und/oder Passwort.",
"Results from DuckDuckGo": "Ergebnisse von DuckDuckGo",
@@ -696,7 +696,7 @@
"This room": "In diesem Raum",
"To link to a room it must have an address.": "Um einen Raum zu verlinken, muss er eine Adresse haben.",
"Undecryptable": "Nicht entschlüsselbar",
- "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Kann nicht feststellen, ob die Adresse an die diese Einladung gesendet wurde mit einer übereinstimmt, die zu deinem Konto gehört.",
+ "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Es konnte nicht ermittelt werden, ob die Adresse, an die diese Einladung gesendet wurde, mit einer mit deinem Benutzerkonto verknüpften Adresse übereinstimmt.",
"Unencrypted message": "Nicht verschlüsselbare Nachricht",
"unknown caller": "Unbekannter Anrufer",
"Unnamed Room": "Unbenannter Raum",
@@ -739,7 +739,7 @@
"Changes colour scheme of current room": "Ändere Farbschema des aktuellen Raumes",
"Delete widget": "Widget entfernen",
"Define the power level of a user": "Setze das Berechtigungslevel eines Benutzers",
- "Edit": "Bearbeiten",
+ "Edit": "Editieren",
"Enable automatic language detection for syntax highlighting": "Automatische Spracherkennung für die Syntax-Hervorhebung aktivieren",
"Hide Apps": "Apps verbergen",
"Hide join/leave messages (invites/kicks/bans unaffected)": "Betreten-/Verlassen-Benachrichtigungen verbergen (gilt nicht für Einladungen/Kicks/Bans)",
@@ -770,15 +770,15 @@
"Do you want to load widget from URL:": "Möchtest du das Widget von folgender URL laden:",
"Integrations Error": "Integrations-Error",
"NOTE: Apps are not end-to-end encrypted": "BEACHTE: Apps sind nicht Ende-zu-Ende verschlüsselt",
- "%(widgetName)s widget added by %(senderName)s": "Widget \"%(widgetName)s\" von %(senderName)s hinzugefügt",
- "%(widgetName)s widget removed by %(senderName)s": "Widget \"%(widgetName)s\" von %(senderName)s entfernt",
+ "%(widgetName)s widget added by %(senderName)s": "%(senderName)s hat das Widget %(widgetName)s hinzugefügt",
+ "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s hat das Widget %(widgetName)s entfernt",
"Robot check is currently unavailable on desktop - please use a web browser": "In der Desktop-Version kann derzeit nicht geprüft werden, ob ein Benutzer ein Roboter ist. Bitte einen Webbrowser verwenden",
"%(widgetName)s widget modified by %(senderName)s": "Das Widget '%(widgetName)s' wurde von %(senderName)s bearbeitet",
"Copied!": "Kopiert!",
"Failed to copy": "Kopieren fehlgeschlagen",
"Ignored Users": "Ignorierte Benutzer",
"Ignore": "Ignorieren",
- "You are now ignoring %(userId)s": "Du ignorierst jetzt %(userId)s",
+ "You are now ignoring %(userId)s": "%(userId)s wird jetzt ignoriert",
"You are no longer ignoring %(userId)s": "%(userId)s wird nicht mehr ignoriert",
"Message removed by %(userId)s": "Nachricht wurde von %(userId)s entfernt",
"Name or matrix ID": "Name oder Matrix-ID",
@@ -793,30 +793,30 @@
"You have entered an invalid address.": "Du hast eine ungültige Adresse eingegeben.",
"Matrix ID": "Matrix-ID",
"Advanced options": "Erweiterte Optionen",
- "Block users on other matrix homeservers from joining this room": "Blockiere Nutzer anderer Matrix-Heimserver die diesen Raum betreten wollen",
+ "Block users on other matrix homeservers from joining this room": "Benutzer anderer Matrix-Heimserver das Betreten dieses Raumes verbieten",
"This setting cannot be changed later!": "Diese Einstellung kann nachträglich nicht mehr geändert werden!",
"Unignore": "Entignorieren",
"User Options": "Benutzer-Optionen",
- "Unignored user": "Benutzer entignoriert",
+ "Unignored user": "Benutzer nicht mehr ignoriert",
"Ignored user": "Benutzer ignoriert",
"Stops ignoring a user, showing their messages going forward": "Beendet das Ignorieren eines Benutzers, nachfolgende Nachrichten werden wieder angezeigt",
"Ignores a user, hiding their messages from you": "Ignoriert einen Benutzer und verbirgt dessen Nachrichten",
"Disable Emoji suggestions while typing": "Emoji-Vorschläge während des Schreibens deaktivieren",
- "Banned by %(displayName)s": "Gebannt von %(displayName)s",
- "To send messages, you must be a": "Um Nachrichten zu senden musst du sein ein",
+ "Banned by %(displayName)s": "Verbannt von %(displayName)s",
+ "To send messages, you must be a": "Notwendiges Berechtigungslevel, um Nachrichten zu senden",
"To invite users into the room, you must be a": "Notwendiges Berechtigungslevel, um Benutzer in diesen Raum einladen zu können:",
- "To configure the room, you must be a": "Notwendiges Berechtigungslevel, um diesen Raum konfigurieren:",
+ "To configure the room, you must be a": "Notwendiges Berechtigungslevel, um diesen Raum zu konfigurieren:",
"To kick users, you must be a": "Notwendiges Berechtigungslevel, um Benutzer zu kicken:",
- "To ban users, you must be a": "Notwendiges Berechtigungslevel, um einen Benutzer zu verbannen:",
- "To remove other users' messages, you must be a": "Um Nachrichten von Benutzern zu löschen, musst du sein ein",
- "To send events of type , you must be a": "Um Ereignisse desTyps zu senden, musst du sein ein",
- "To change the room's avatar, you must be a": "Um das Raumbild zu ändern, musst du sein ein",
- "To change the room's name, you must be a": "Um den Raumnamen zu ändern, musst du sein ein",
- "To change the room's main address, you must be a": "Um die Hauptadresse des Raumes zu ändern, musst du sein ein",
- "To change the room's history visibility, you must be a": "Um die Sichtbarkeit des bisherigen Chatverlaufs zu ändern, musst du sein ein",
- "To change the permissions in the room, you must be a": "Um Berechtigungen in diesem Raum zu ändern, musst du sein ein",
- "To change the topic, you must be a": "Um das Thema zu ändern, musst du sein ein",
- "To modify widgets in the room, you must be a": "Um Widgets in dem Raum zu ändern, musst du sein ein",
+ "To ban users, you must be a": "Notwendiges Berechtigungslevel, um Benutzer zu verbannen:",
+ "To remove other users' messages, you must be a": "Notwendiges Berechtigungslevel, um Nachrichten von anderen Benutzern zu löschen",
+ "To send events of type , you must be a": "Notwendiges Berechtigungslevel, um Ereignisse des Typs zu senden",
+ "To change the room's avatar, you must be a": "Notwendiges Berechtigungslevel, um das Raumbild zu ändern",
+ "To change the room's name, you must be a": "Notwendiges Berechtigungslevel, um den Raumnamen zu ändern",
+ "To change the room's main address, you must be a": "Notwendiges Berechtigungslevel, um die Hauptadresse des Raumes zu ändern",
+ "To change the room's history visibility, you must be a": "Notwendiges Berechtigungslevel, um die Sichtbarkeit des bisherigen Chatverlaufs zu ändern",
+ "To change the permissions in the room, you must be a": "Notwendiges Berechtigungslevel, um Berechtigungen in diesem Raum zu ändern",
+ "To change the topic, you must be a": "Notwendiges Berechtigungslevel, um das Thema zu ändern",
+ "To modify widgets in the room, you must be a": "Notwendiges Berechtigungslevel, um Widgets in diesem Raum zu ändern",
"Description": "Beschreibung",
"Unable to accept invite": "Einladung kann nicht akzeptiert werden",
"Failed to invite users to %(groupId)s": "Benutzer konnten nicht in %(groupId)s eingeladen werden",
@@ -826,16 +826,16 @@
"Failed to add the following users to the summary of %(groupId)s:": "Die folgenden Benutzer konnten nicht zur Übersicht von %(groupId)s hinzugefügt werden:",
"Which rooms would you like to add to this summary?": "Welche Räume möchtest du zu dieser Übersicht hinzufügen?",
"Room name or alias": "Raum-Name oder Alias",
- "Failed to add the following rooms to the summary of %(groupId)s:": "Folgende Räume konnten nicht zur Übersicht von %(groupId)s hinzugefügt werden:",
- "Failed to remove the room from the summary of %(groupId)s": "Raum konnte nicht aus der Übersicht von %(groupId)s entfernt werden",
+ "Failed to add the following rooms to the summary of %(groupId)s:": "Die folgenden Räume konnten nicht zur Übersicht von %(groupId)s hinzugefügt werden:",
+ "Failed to remove the room from the summary of %(groupId)s": "Der Raum konnte nicht aus der Übersicht von %(groupId)s entfernt werden",
"The room '%(roomName)s' could not be removed from the summary.": "Der Raum '%(roomName)s' konnte nicht aus der Übersicht entfernt werden.",
"Failed to remove a user from the summary of %(groupId)s": "Benutzer konnte nicht aus der Übersicht von %(groupId)s entfernt werden",
"The user '%(displayName)s' could not be removed from the summary.": "Der Benutzer '%(displayName)s' konnte nicht aus der Übersicht entfernt werden.",
"Unknown": "Unbekannt",
- "Failed to add the following rooms to %(groupId)s:": "Die folgenden Räume konnten %(groupId)s nicht hinzugefügt werden:",
+ "Failed to add the following rooms to %(groupId)s:": "Die folgenden Räume konnten nicht zu %(groupId)s hinzugefügt werden:",
"Matrix Room ID": "Matrix-Raum-ID",
"email address": "E-Mail-Adresse",
- "Try using one of the following valid address types: %(validTypesList)s.": "Versuche eine der folgenden validen Adresstypen zu benutzen: %(validTypesList)s.",
+ "Try using one of the following valid address types: %(validTypesList)s.": "Bitte einen der folgenden gültigen Adresstypen verwenden: %(validTypesList)s.",
"Failed to remove '%(roomName)s' from %(groupId)s": "Entfernen von '%(roomName)s' aus %(groupId)s fehlgeschlagen",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Bist du sicher, dass du '%(roomName)s' aus '%(groupId)s' entfernen möchtest?",
"Invites sent": "Einladungen gesendet",
@@ -845,17 +845,161 @@
"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": "Nachricht-Anheftung",
+ "Message Pinning": "Anheften von Nachrichten",
"Publish this community on your profile": "Diese Community in deinem Profil veröffentlichen",
"Long Description (HTML)": "Lange Beschreibung (HTML)",
"Jump to message": "Zur Nachricht springen",
- "No pinned messages.": "Keine angehefteten Nachrichten.",
+ "No pinned messages.": "Keine angehefteten Nachrichten vorhanden.",
"Loading...": "Lade...",
- "Unpin Message": "Nachricht losheften",
+ "Unpin Message": "Nachricht nicht mehr anheften",
"Unnamed room": "Unbenannter Raum",
- "World readable": "Lesbar für die Welt",
+ "World readable": "Lesbar für alle",
"Guests can join": "Gäste können beitreten",
- "No rooms to show": "Keine Räume anzuzeigen",
+ "No rooms to show": "Keine anzeigbaren Räume",
"Community Settings": "Community-Einstellungen",
- "Community Member Settings": "Community-Mitglieder-Einstellungen"
+ "Community Member Settings": "Community-Mitglieder-Einstellungen",
+ "Who would you like to add to this community?": "Wen möchtest du zu dieser Community hinzufügen?",
+ "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warnung: Jede Person, die du einer Community hinzufügst, wird für alle, die die Community-ID kennen, öffentlich sichtbar sein",
+ "Invite new community members": "Neue Community-Mitglieder einladen",
+ "Invite to Community": "In die Community einladen",
+ "Which rooms would you like to add to this community?": "Welche Räume möchtest du zu dieser Community hinzufügen?",
+ "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID": "Warnung: Jeder Raum, den du zu einer Community hinzufügst, wird für alle, die die Community-ID kennen, öffentlich sichtbar sein",
+ "Add rooms to the community": "Räume zur Community hinzufügen",
+ "Add to community": "Zur Community hinzufügen",
+ "Your community invitations have been sent.": "Deine Community-Einladungen wurden gesendet.",
+ "Failed to invite users to community": "Benutzer konnten nicht in die Community eingeladen werden",
+ "Communities": "Communities",
+ "Invalid community ID": "Ungültige Community-ID",
+ "'%(groupId)s' is not a valid community ID": "'%(groupId)s' ist keine gültige Community-ID",
+ "Related Communities": "Verknüpfte Communities",
+ "Related communities for this room:": "Verknüpfte Communities für diesen Raum:",
+ "This room has no related communities": "Dieser Raum hat keine verknüpften Communities",
+ "New community ID (e.g. +foo:%(localDomain)s)": "Neue Community-ID (z. B. +foo:%(localDomain)s)",
+ "Remove from community": "Aus Community entfernen",
+ "Failed to remove user from community": "Entfernen des Benutzers aus der Community fehlgeschlagen",
+ "Filter community members": "Community-Mitglieder filtern",
+ "Filter community rooms": "Community-Räume filtern",
+ "Failed to remove room from community": "Entfernen des Raumes aus der Community fehlgeschlagen",
+ "Removing a room from the community will also remove it from the community page.": "Ein Entfernen eines Raumes aus der Community wird ihn auch von der Community-Seite entfernen.",
+ "Community IDs may only contain alphanumeric characters": "Community-IDs dürfen nur alphanumerische Zeichen enthalten",
+ "Create Community": "Community erstellen",
+ "Community Name": "Community-Name",
+ "Community ID": "Community-ID",
+ "example": "Beispiel",
+ "Add rooms to the community summary": "Fügt Räume zur Community-Übersicht hinzu",
+ "Add users to the community summary": "Fügt Benutzer zur Community-Übersicht hinzu",
+ "Failed to update community": "Aktualisieren der Community fehlgeschlagen",
+ "Leave Community": "Community verlassen",
+ "Add rooms to this community": "Räume zu dieser Community hinzufügen",
+ "%(inviter)s has invited you to join this community": "%(inviter)s hat dich in diese Community eingeladen",
+ "You are a member of this community": "Du bist ein Mitglied dieser Community",
+ "You are an administrator of this community": "Du bist ein Administrator dieser Community",
+ "Community %(groupId)s not found": "Community '%(groupId)s' nicht gefunden",
+ "This Home server does not support communities": "Dieser Heimserver unterstützt keine Communities",
+ "Failed to load %(groupId)s": "'%(groupId)s' konnte nicht geladen werden",
+ "Error whilst fetching joined communities": "Fehler beim Laden beigetretener Communities",
+ "Create a new community": "Neue Community erstellen",
+ "Create a community to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.": "Erzeuge eine Community um deine Community zu repräsentieren! Definiere eine Menge von Räumen und deine eigene angepasste Startseite um dein Revier im Matrix-Universum zu markieren.",
+ "Join an existing community": "Einer bestehenden Community beitreten",
+ "To join an existing community you'll have to know its community identifier; this will look something like +example:matrix.org.": "Um einer bereits bestehenden Community beitreten zu können, musst dir deren Community-ID bekannt sein. Diese sieht z. B. aus wie +example:matrix.org.",
+ "Your Communities": "Deine Communities",
+ "You're not currently a member of any communities.": "Du bist aktuell kein Mitglied einer Community.",
+ "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Erzeuge eine Community um Nutzer und Räume zu gruppieren! Erzeuge eine angepasste Homepage um dein Revier im Matrix-Universum zu markieren.",
+ "Something went wrong whilst creating your community": "Beim Erstellen deiner Community ist ein Fehler aufgetreten",
+ "%(names)s and %(count)s others are typing|other": "%(names)s und %(count)s weitere schreiben",
+ "And %(count)s more...|other": "Und %(count)s weitere...",
+ "Delete Widget": "Widget löschen",
+ "Message removed": "Nachricht entfernt",
+ "Mention": "Erwähnen",
+ "Invite": "Einladen",
+ "Remove this room from the community": "Diesen Raum aus der Community entfernen",
+ "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Das Löschen eines Widgets entfernt das Widget für alle Benutzer in diesem Raum. Möchtest du dieses Widget wirklich löschen?",
+ "Mirror local video feed": "Lokalen Video-Feed spiegeln",
+ "Failed to withdraw invitation": "Die Einladung konnte nicht zurückgezogen werden",
+ "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Community-IDs dürfen nur die folgenden Zeichen enthalten: a-z, 0-9, or '=_-./'",
+ "%(senderName)s sent an image": "%(senderName)s hat ein Bild gesendet",
+ "%(senderName)s sent a video": "%(senderName)s hat ein Video gesendet",
+ "%(senderName)s uploaded a file": "%(senderName)s hat eine Datei hochgeladen",
+ "You have been banned from this room by %(userName)s.": "%(userName)s hat dich aus diesem Raum verbannt.",
+ "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
+ "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)shaben den Raum %(count)s-mal betreten",
+ "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)shaben den Raum betreten",
+ "%(oneUser)sjoined %(count)s times|other": "%(oneUser)shat den Raum %(count)s-mal betreten",
+ "%(oneUser)sjoined %(count)s times|one": "%(oneUser)shat den Raum betreten",
+ "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)shaben den Raum %(count)s-mal verlassen",
+ "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)shaben den Raum verlassen",
+ "%(oneUser)sleft %(count)s times|other": "%(oneUser)shat den Raum %(count)s-mal verlassen",
+ "%(oneUser)sleft %(count)s times|one": "%(oneUser)shat den Raum verlassen",
+ "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)shaben %(count)s-mal den Raum betreten und verlassen",
+ "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)shaben den Raum betreten und wieder verlassen",
+ "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)shat den Raum %(count)s-mal betreten und wieder verlassen",
+ "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)shat den Raum betreten und wieder verlassen",
+ "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)shaben den Raum %(count)s-mal verlassen und wieder betreten",
+ "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)shaben den Raum verlassen und wieder betreten",
+ "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)shat den Raum %(count)s-mal verlassen und wieder betreten",
+ "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)shat den Raum verlassen und wieder betreten",
+ "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)shaben ihre Einladungen abgelehnt",
+ "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)swurde die Einladung %(count)s-mal wieder entzogen",
+ "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)swurde die Einladung wieder entzogen",
+ "were invited %(count)s times|other": "wurden %(count)s-mal eingeladen",
+ "were invited %(count)s times|one": "wurden eingeladen",
+ "was invited %(count)s times|other": "wurde %(count)s-mal eingeladen",
+ "was invited %(count)s times|one": "wurde eingeladen",
+ "were banned %(count)s times|other": "wurden %(count)s-mal verbannt",
+ "were banned %(count)s times|one": "wurden verbannt",
+ "was banned %(count)s times|other": "wurde %(count)s-mal verbannt",
+ "was banned %(count)s times|one": "wurde verbannt",
+ "were kicked %(count)s times|other": "wurden %(count)s-mal gekickt",
+ "were kicked %(count)s times|one": "wurden gekickt",
+ "was kicked %(count)s times|other": "wurde %(count)s-mal gekickt",
+ "was kicked %(count)s times|one": "wurde gekickt",
+ "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)shaben %(count)s-mal ihren Namen geändert",
+ "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)shaben ihren Namen geändert",
+ "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)shat %(count)s-mal den Namen geändert",
+ "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)shaben das Profilbild %(count)s-mal geändert",
+ "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)shaben das Profilbild geändert",
+ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)shat das Profilbild %(count)s-mal geändert",
+ "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)shat das Profilbild geändert",
+ "%(names)s and %(count)s others are typing|one": "%(names)s und eine weitere Person schreiben",
+ "Disinvite this user?": "Einladung für diesen Benutzer zurückziehen?",
+ "Kick this user?": "Diesen Benutzer kicken?",
+ "Unban this user?": "Verbannung dieses Benutzers aufheben?",
+ "Ban this user?": "Diesen Benutzer verbannen?",
+ "Drop here to favourite": "Hierher ziehen, um als Favorit zu markieren",
+ "Drop here to tag direct chat": "Hierher ziehen, um als Direkt-Chat zu markieren",
+ "Drop here to restore": "Hierher ziehen zum Wiederherstellen",
+ "Drop here to demote": "Hier loslassen um zurückzustufen",
+ "You have been kicked from this room by %(userName)s.": "Du wurdest von %(userName)s aus diesem Raum gekickt.",
+ "You are trying to access a room.": "Du versuchst, auf einen Raum zuzugreifen.",
+ "Members only (since the point in time of selecting this option)": "Nur Mitglieder (ab dem Zeitpunkt, an dem diese Option ausgewählt wird)",
+ "Members only (since they were invited)": "Nur Mitglieder (ab dem Zeitpunkt, an dem sie eingeladen wurden)",
+ "Members only (since they joined)": "Nur Mitglieder (ab dem Zeitpunkt, an dem sie beigetreten sind)",
+ "An email has been sent to %(emailAddress)s": "Eine E-Mail wurde an %(emailAddress)s gesendet",
+ "A text message has been sent to %(msisdn)s": "Eine Textnachricht wurde an %(msisdn)s gesendet",
+ "Disinvite this user from community?": "Community-Einladung für diesen Benutzer zurückziehen?",
+ "Remove this user from community?": "Diesen Benutzer aus der Community entfernen?",
+ "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)shaben ihre Einladungen %(count)s-mal abgelehnt",
+ "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)shat die Einladung %(count)s-mal abgelehnt",
+ "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)shat die Einladung abgelehnt",
+ "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)swurde die Einladung %(count)s-mal wieder entzogen",
+ "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)swurde die Einladung wieder entzogen",
+ "were unbanned %(count)s times|other": "wurden %(count)s-mal entbannt",
+ "were unbanned %(count)s times|one": "wurden entbannt",
+ "was unbanned %(count)s times|other": "wurde %(count)s-mal entbannt",
+ "was unbanned %(count)s times|one": "wurde entbannt",
+ "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)shat den Namen geändert",
+ "%(items)s and %(count)s others|other": "%(items)s und %(count)s andere",
+ "%(items)s and %(count)s others|one": "%(items)s und ein anderer",
+ "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Eine E-Mail wurde an %(emailAddress)s gesendet. Folge dem in der E-Mail enthaltenen Link und klicke dann unten.",
+ "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "Die Sichtbarkeit von '%(roomName)s' in %(groupId)s konnte nicht aktualisiert werden.",
+ "Visibility in Room List": "Sichtbarkeit in Raum-Liste",
+ "Visible to everyone": "Für jeden sichtbar",
+ "Only visible to community members": "Nur für Community-Mitglieder sichtbar",
+ "Community Invites": "Community-Einladungen",
+ "Notify the whole room": "Den gesamten Raum benachrichtigen",
+ "Room Notification": "Raum-Benachrichtigung",
+ "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Diese Räume werden Community-Mitgliedern auf der Community-Seite angezeigt. Community-Mitglieder können diesen Räumen beitreten, indem sie auf diese klicken.",
+ "Show these rooms to non-members on the community page and room list?": "Sollen diese Räume Nicht-Mitgliedern auf der Community-Seite und Raum-Liste gezeigt werden?",
+ "
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": "
HTML für deine Community-Seite
\n
\n Nutze die lange Beschreibung um neuen Mitgliedern diese Community zu beschreiben\n oder um einige wichtige Informationen oder Links festzuhalten.\n
\n
\n Du kannst auch 'img'-Tags (HTML) verwenden\n
\n",
+ "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!": "Deine Community hat noch keine lange Beschreibung oder eine HTML-Seite die Community-Mitgliedern gezeigt wird. Klicke hier um die Einstellungen zu öffnen und ihr eine zu geben!"
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 2039d09368..dfa11f44c9 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -49,7 +49,7 @@
"Name or matrix ID": "Name or matrix ID",
"Invite to Community": "Invite to Community",
"Which rooms would you like to add to this community?": "Which rooms would you like to add to this community?",
- "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID",
+ "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?",
"Add rooms to the community": "Add rooms to the community",
"Room name or alias": "Room name or alias",
"Add to community": "Add to community",
@@ -63,7 +63,7 @@
"This email address was not found": "This email address was not found",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.",
"Default": "Default",
- "User": "User",
+ "Restricted": "Restricted",
"Moderator": "Moderator",
"Admin": "Admin",
"Start a chat": "Start a chat",
@@ -150,8 +150,6 @@
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
- "Communities": "Communities",
- "Message Pinning": "Message Pinning",
"%(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",
@@ -163,6 +161,33 @@
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
"Failed to join room": "Failed to join room",
+ "Message Pinning": "Message Pinning",
+ "Presence Management": "Presence Management",
+ "Tag Panel": "Tag Panel",
+ "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
+ "Use compact timeline layout": "Use compact timeline layout",
+ "Hide removed messages": "Hide removed messages",
+ "Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)",
+ "Hide avatar changes": "Hide avatar changes",
+ "Hide display name changes": "Hide display name changes",
+ "Hide read receipts": "Hide read receipts",
+ "Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
+ "Always show message timestamps": "Always show message timestamps",
+ "Autoplay GIFs and videos": "Autoplay GIFs and videos",
+ "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
+ "Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
+ "Disable big emoji in chat": "Disable big emoji in chat",
+ "Don't send typing notifications": "Don't send typing notifications",
+ "Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
+ "Mirror local video feed": "Mirror local video feed",
+ "Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls",
+ "Opt out of analytics": "Opt out of analytics",
+ "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",
+ "Never send encrypted messages to unverified devices in this room from this device": "Never send encrypted messages to unverified devices in this room from this device",
+ "Enable inline URL previews by default": "Enable inline URL previews by default",
+ "Enable URL previews for this room (only affects you)": "Enable URL previews for this room (only affects you)",
+ "Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room",
+ "Room Colour": "Room Colour",
"Active call (%(roomName)s)": "Active call (%(roomName)s)",
"unknown caller": "unknown caller",
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
@@ -194,13 +219,14 @@
"Change Password": "Change Password",
"Your home server does not support device management.": "Your home server does not support device management.",
"Unable to load device list": "Unable to load device list",
+ "Authentication": "Authentication",
+ "Delete %(count)s devices|other": "Delete %(count)s devices",
+ "Delete %(count)s devices|one": "Delete device",
"Device ID": "Device ID",
"Device Name": "Device Name",
"Last seen": "Last seen",
+ "Select devices": "Select devices",
"Failed to set display name": "Failed to set display name",
- "Authentication": "Authentication",
- "Failed to delete device": "Failed to delete device",
- "Delete": "Delete",
"Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications",
"Cannot add any more widgets": "Cannot add any more widgets",
@@ -295,10 +321,14 @@
"No pinned messages.": "No pinned messages.",
"Loading...": "Loading...",
"Pinned Messages": "Pinned Messages",
- "for %(amount)ss": "for %(amount)ss",
- "for %(amount)sm": "for %(amount)sm",
- "for %(amount)sh": "for %(amount)sh",
- "for %(amount)sd": "for %(amount)sd",
+ "%(duration)ss": "%(duration)ss",
+ "%(duration)sm": "%(duration)sm",
+ "%(duration)sh": "%(duration)sh",
+ "%(duration)sd": "%(duration)sd",
+ "Online for %(duration)s": "Online for %(duration)s",
+ "Idle for %(duration)s": "Idle for %(duration)s",
+ "Offline for %(duration)s": "Offline for %(duration)s",
+ "Unknown for %(duration)s": "Unknown for %(duration)s",
"Online": "Online",
"Idle": "Idle",
"Offline": "Offline",
@@ -370,7 +400,6 @@
"Devices will not yet be able to decrypt history from before they joined the room": "Devices will not yet be able to decrypt history from before they joined the room",
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Once encryption is enabled for a room it cannot be turned off again (for now)",
"Encrypted messages will not be visible on clients that do not yet implement encryption": "Encrypted messages will not be visible on clients that do not yet implement encryption",
- "Never send encrypted messages to unverified devices in this room from this device": "Never send encrypted messages to unverified devices in this room from this device",
"Enable encryption": "Enable encryption",
"(warning: cannot be disabled again!)": "(warning: cannot be disabled again!)",
"Encryption is enabled in this room": "Encryption is enabled in this room",
@@ -396,7 +425,6 @@
"Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)",
"Members only (since they were invited)": "Members only (since they were invited)",
"Members only (since they joined)": "Members only (since they joined)",
- "Room Colour": "Room Colour",
"Permissions": "Permissions",
"The default role for new room members is": "The default role for new room members is",
"To send messages, you must be a": "To send messages, you must be a",
@@ -427,19 +455,15 @@
"New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)",
"Invalid community ID": "Invalid community ID",
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID",
- "Related Communities": "Related Communities",
- "Related communities for this room:": "Related communities for this room:",
- "This room has no related communities": "This room has no related communities",
+ "Flair": "Flair",
+ "Showing flair for these communities:": "Showing flair for these communities:",
+ "This room is not showing flair for any communities": "This room is not showing flair for any communities",
"New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)",
- "Disable URL previews by default for participants in this room": "Disable URL previews by default for participants in this room",
- "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.": "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.",
- "disabled": "disabled",
- "enabled": "enabled",
- "You have disabled URL previews by default.": "You have disabled URL previews by default.",
"You have enabled URL previews by default.": "You have enabled URL previews by default.",
+ "You have disabled URL previews by default.": "You have disabled URL previews by default.",
+ "URL previews are enabled by default for participants in this room.": "URL previews are enabled by default for participants in this room.",
+ "URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants in this room.",
"URL Previews": "URL Previews",
- "Enable URL previews for this room (affects only you)": "Enable URL previews for this room (affects only you)",
- "Disable URL previews for this room (affects only you)": "Disable URL previews for this room (affects only you)",
"Error decrypting audio": "Error decrypting audio",
"Error decrypting attachment": "Error decrypting attachment",
"Decrypt %(text)s": "Decrypt %(text)s",
@@ -476,6 +500,7 @@
"Please enter the code it contains:": "Please enter the code it contains:",
"Start authentication": "Start authentication",
"powered by Matrix": "powered by Matrix",
+ "Username on %(hs)s": "Username on %(hs)s",
"User name": "User name",
"Mobile phone number": "Mobile phone number",
"Forgot your password?": "Forgot your password?",
@@ -499,6 +524,8 @@
"Failed to withdraw invitation": "Failed to withdraw invitation",
"Failed to remove user from community": "Failed to remove user from community",
"Filter community members": "Filter community members",
+ "Flair will appear if enabled in room settings": "Flair will appear if enabled in room settings",
+ "Flair will not appear": "Flair will not appear",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?",
"Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.",
"Remove": "Remove",
@@ -510,6 +537,9 @@
"Visible to everyone": "Visible to everyone",
"Only visible to community members": "Only visible to community members",
"Filter community rooms": "Filter community rooms",
+ "Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.",
+ "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.",
"Unknown Address": "Unknown Address",
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
"Do you want to load widget from URL:": "Do you want to load widget from URL:",
@@ -525,6 +555,8 @@
"Unverify": "Unverify",
"Verify...": "Verify...",
"No results": "No results",
+ "Delete": "Delete",
+ "Communities": "Communities",
"Home": "Home",
"Integrations Error": "Integrations Error",
"Could not connect to the integration server": "Could not connect to the integration server",
@@ -583,6 +615,7 @@
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
"collapse": "collapse",
"expand": "expand",
+ "Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
"Custom level": "Custom level",
"Room directory": "Room directory",
"Start chat": "Start chat",
@@ -705,8 +738,6 @@
"%(inviter)s has invited you to join this community": "%(inviter)s has invited you to join this community",
"You are an administrator of this community": "You are an administrator of this community",
"You are a member of this community": "You are a member of this community",
- "Community Member Settings": "Community Member Settings",
- "Publish this community on your profile": "Publish this community on your profile",
"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!": "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!",
"Long Description (HTML)": "Long Description (HTML)",
"Description": "Description",
@@ -722,7 +753,6 @@
"For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.",
"Logout": "Logout",
"Your Communities": "Your Communities",
- "You're not currently a member of any communities.": "You're not currently a member of any communities.",
"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.",
@@ -732,11 +762,11 @@
"Scroll to bottom of page": "Scroll to bottom of page",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
- "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Resend all or cancel all now. You can also select individual messages to resend or cancel.",
+ "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Resend all or cancel all now. You can also select individual messages to resend or cancel.",
"%(count)s new messages|other": "%(count)s new messages",
"%(count)s new messages|one": "%(count)s new message",
"Active call": "Active call",
- "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?",
+ "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
"Some of your messages have not been sent.": "Some of your messages have not been sent.",
@@ -761,26 +791,9 @@
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
- "Autoplay GIFs and videos": "Autoplay GIFs and videos",
- "Hide read receipts": "Hide read receipts",
- "Don't send typing notifications": "Don't send typing notifications",
- "Always show message timestamps": "Always show message timestamps",
- "Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
- "Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)",
- "Hide avatar and display name changes": "Hide avatar and display name changes",
- "Use compact timeline layout": "Use compact timeline layout",
- "Hide removed messages": "Hide removed messages",
- "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
- "Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
- "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
- "Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
- "Disable big emoji in chat": "Disable big emoji in chat",
- "Mirror local video feed": "Mirror local video feed",
- "Opt out of analytics": "Opt out of analytics",
- "Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls",
- "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",
"Light theme": "Light theme",
"Dark theme": "Dark theme",
+ "Status.im theme": "Status.im theme",
"Can't load user settings": "Can't load user settings",
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
"Sign out": "Sign out",
@@ -795,7 +808,6 @@
"Interface Language": "Interface Language",
"User Interface": "User Interface",
"Autocomplete Delay (ms):": "Autocomplete Delay (ms):",
- "Disable inline URL previews by default": "Disable inline URL previews by default",
"": "",
"Import E2E room keys": "Import E2E room keys",
"Cryptography": "Cryptography",
@@ -860,14 +872,15 @@
"Create an account": "Create an account",
"This Home Server does not support login using email address.": "This Home Server does not support login using email address.",
"Incorrect username and/or password.": "Incorrect username and/or password.",
+ "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
"Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.",
"The phone number entered looks invalid": "The phone number entered looks invalid",
+ "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.",
"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.": "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.",
- "Sorry, this homeserver is using a login which is not recognised ": "Sorry, this homeserver is using a login which is not recognised ",
"Login as guest": "Login as guest",
- "Return to app": "Return to app",
+ "Sign in to get started": "Sign in to get started",
"Failed to fetch avatar URL": "Failed to fetch avatar URL",
"Set a display name:": "Set a display name:",
"Upload an avatar:": "Upload an avatar:",
diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json
index 0967ef424b..363b61faad 100644
--- a/src/i18n/strings/eo.json
+++ b/src/i18n/strings/eo.json
@@ -1 +1,43 @@
-{}
+{
+ "This email address is already in use": "Tiu ĉi retpoŝtadreso jam estas uzata",
+ "This phone number is already in use": "Tiu ĉi telefona numero jam estas uzata",
+ "Failed to verify email address: make sure you clicked the link in the email": "Kontrolo de via retpoŝtadreso malsukcesis; certigu, ke vi alklakis la ligilon en la retletero",
+ "Call Timeout": "Voka Tempolimo",
+ "The remote side failed to pick up": "Kunvokonto malsukcesis respondi",
+ "Unable to capture screen": "Ekrano ne registreblas",
+ "You cannot place a call with yourself.": "Vi ne povas voki vin mem.",
+ "Warning!": "Averto!",
+ "Sign in with CAS": "Saluti per CAS",
+ "Sign in with": "Saluti per",
+ "Sign in": "Saluti",
+ "For security, this session has been signed out. Please sign in again.": "Pro sekurecaj kialoj, la seanco finiĝis. Bonvolu resaluti.",
+ "Upload Failed": "Alŝuto malsukcesis",
+ "Sun": "Dim",
+ "Mon": "Lun",
+ "Tue": "Mar",
+ "Wed": "Mer",
+ "Thu": "Ĵaŭ",
+ "Fri": "Ven",
+ "Sat": "Sab",
+ "Jan": "Jan",
+ "Feb": "Feb",
+ "Mar": "Mar",
+ "Apr": "Apr",
+ "May": "Maj",
+ "Jun": "Jun",
+ "Jul": "Jul",
+ "Aug": "Aŭg",
+ "Sep": "Sep",
+ "Oct": "Okt",
+ "Nov": "Nov",
+ "Dec": "Dec",
+ "PM": "ptm",
+ "AM": "atm",
+ "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
+ "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
+ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(fullYear)s %(monthName)s %(day)s %(time)s",
+ "Who would you like to add to this community?": "Kiun vi volas aldoni al tiu ĉi komunumo?",
+ "Invite new community members": "Invitu novajn komunumanojn",
+ "Name or matrix ID": "Nomo aŭ Matrix-identigilo",
+ "Invite to Community": "Inviti al komunumo"
+}
diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index 815960c4cc..a9b7232e29 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -370,7 +370,7 @@
"Warning!": "Attention !",
"Who can access this room?": "Qui peut accéder au salon ?",
"Who can read history?": "Qui peut lire l'historique ?",
- "Who would you like to add to this room?": "Qui voulez-vous inviter dans ce salon ?",
+ "Who would you like to add to this room?": "Qui voulez-vous ajouter à ce salon ?",
"Who would you like to communicate with?": "Avec qui voulez-vous communiquer ?",
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s a annulé l’invitation de %(targetName)s.",
"You are already in a call.": "Vous avez déjà un appel en cours.",
@@ -433,7 +433,7 @@
"There are no visible files in this room": "Il n'y a pas de fichier visible dans ce salon",
"Room": "Salon",
"Connectivity to the server has been lost.": "La connectivité au serveur a été perdue.",
- "Sent messages will be stored until your connection has returned.": "Les messages envoyés seront stockés jusqu’à ce que votre connection revienne.",
+ "Sent messages will be stored until your connection has returned.": "Les messages envoyés seront stockés jusqu’à ce que votre connexion revienne.",
"Cancel": "Annuler",
"Active call": "Appel en cours",
"code": "code",
@@ -774,5 +774,228 @@
"Failed to copy": "Échec de la copie",
"Verifies a user, device, and pubkey tuple": "Vérifie un utilisateur, un appareil et une clé publique",
"%(widgetName)s widget modified by %(senderName)s": "Widget %(widgetName)s modifié par %(senderName)s",
- "Robot check is currently unavailable on desktop - please use a web browser": "La vérification robot n'est pas encore disponible pour le bureau - veuillez utiliser un navigateur"
+ "Robot check is currently unavailable on desktop - please use a web browser": "La vérification robot n'est pas encore disponible pour le bureau - veuillez utiliser un navigateur",
+ "Who would you like to add to this community?": "Qui souhaitez-vous ajouter à cette communauté ?",
+ "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Attention : toute personne ajoutée à une communauté sera visible par tous ceux connaissant l'identifiant de la communauté",
+ "Invite new community members": "Inviter de nouveaux membres dans cette communauté",
+ "Name or matrix ID": "Nom ou identifiant matrix",
+ "Which rooms would you like to add to this community?": "Quels salons souhaitez-vous ajouter à cette communauté ?",
+ "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID": "Attention : tout salon ajouté à une communauté est visible par quiconque connaissant l'identifiant de la communauté",
+ "Add rooms to the community": "Ajouter des salons à la communauté",
+ "Room name or alias": "Nom du salon ou alias",
+ "Add to community": "Ajouter à la communauté",
+ "Failed to invite the following users to %(groupId)s:": "Échec de l'invitation des utilisateurs à %(groupId)s :",
+ "Failed to invite users to community": "Échec de l'invitation d'utilisateurs à la communauté",
+ "Failed to invite users to %(groupId)s": "Échec de l'invitation d'utilisateurs à %(groupId)s",
+ "Failed to add the following rooms to %(groupId)s:": "Échec de l'ajout des salons suivants à %(groupId)s :",
+ "Ignored user": "Utilisateur ignoré",
+ "You are now ignoring %(userId)s": "Dorénavant vous ignorez %(userId)s",
+ "Unignored user": "Utilisateur n'étant plus ignoré",
+ "You are no longer ignoring %(userId)s": "Vous n'ignorez plus %(userId)s",
+ "Invite to Community": "Inviter dans la Communauté",
+ "Communities": "Communautés",
+ "Message Pinning": "Épingler un message",
+ "Mention": "Mentionner",
+ "Unignore": "Ne plus ignorer",
+ "Ignore": "Ignorer",
+ "Invite": "Inviter",
+ "User Options": "Options d'utilisateur",
+ "Admin Tools": "Outils d'administration",
+ "Unpin Message": "Dépingler le message",
+ "Jump to message": "Aller au message",
+ "No pinned messages.": "Aucun message épinglé.",
+ "Loading...": "Chargement...",
+ "Pinned Messages": "Messages épinglés",
+ "Unknown": "Inconnu",
+ "Unnamed room": "Salon sans nom",
+ "No rooms to show": "Aucun salon à afficher",
+ "Remove avatar": "Supprimer l'avatar",
+ "To change the room's avatar, you must be a": "Pour modifier l'avatar du salon, vous devez être un",
+ "To change the room's name, you must be a": "Pour changer le nom du salon, vous devez être un",
+ "To change the room's main address, you must be a": "Pour changer l'adresse principale du salon, vous devez être un",
+ "To change the room's history visibility, you must be a": "Pour changer la visibilité de l'historique d'un salon, vous devez être un",
+ "To change the permissions in the room, you must be a": "Pour changer les autorisations du salon, vous devez être un",
+ "To change the topic, you must be a": "Pour changer le sujet, vous devez être un",
+ "To modify widgets in the room, you must be a": "Pour modifier les widgets, vous devez être un",
+ "Banned by %(displayName)s": "Banni par %(displayName)s",
+ "To send messages, you must be a": "Pour envoyer des messages, vous devez être un",
+ "%(senderName)s changed the pinned messages for the room.": "%(senderName)s a changé les messages épinglés du salon.",
+ "%(names)s and %(count)s others are typing|other": "%(names)s et %(count)s autres écrivent",
+ "Jump to read receipt": "Aller à l'accusé de lecture",
+ "World readable": "Lisible publiquement",
+ "Guests can join": "Les invités peuvent rejoindre le salon",
+ "To invite users into the room, you must be a": "Pour inviter des utilisateurs dans le salon, vous devez être un",
+ "To configure the room, you must be a": "Pour configurer le salon, vous devez être un",
+ "To kick users, you must be a": "Pour exclure des utilisateurs, vous devez être un",
+ "To ban users, you must be a": "Pour bannir des utilisateurs, vous devez être un",
+ "To remove other users' messages, you must be a": "Pour supprimer les messages d'autres utilisateurs, vous devez être un",
+ "To send events of type , you must be a": "Pour envoyer des évènements du type , vous devez être un",
+ "Invalid community ID": "Identifiant de communauté non valide",
+ "'%(groupId)s' is not a valid community ID": "\"%(groupId)s\" n'est pas un identifiant de communauté valide",
+ "Related Communities": "Communautés associées",
+ "Related communities for this room:": "Communautés associées à ce salon :",
+ "This room has no related communities": "Ce salon n'est associé à aucune communauté",
+ "%(names)s and %(count)s others are typing|one": "%(names)s et un autre écrivent",
+ "%(senderName)s sent an image": "%(senderName)s a envoyé une image",
+ "%(senderName)s sent a video": "%(senderName)s a envoyé une vidéo",
+ "%(senderName)s uploaded a file": "%(senderName)s a transféré un fichier",
+ "Disinvite this user?": "Désinviter l'utilisateur ?",
+ "Kick this user?": "Exclure cet utilisateur ?",
+ "Unban this user?": "Révoquer le bannissement de cet utilisateur ?",
+ "Ban this user?": "Bannir cet utilisateur ?",
+ "Drop here to favourite": "Déposer ici pour mettre en favori",
+ "Drop here to tag direct chat": "Déposer ici pour marquer comme conversation directe",
+ "Drop here to restore": "Déposer ici pour restaurer",
+ "Drop here to demote": "Déposer ici pour rétrograder",
+ "You have been kicked from this room by %(userName)s.": "Vous avez été exclu de ce salon par %(userName)s.",
+ "You have been banned from this room by %(userName)s.": "Vous avez été banni de ce salon par %(userName)s.",
+ "You are trying to access a room.": "Vous essayez d'accéder à un salon.",
+ "Members only (since the point in time of selecting this option)": "Seulement les membres (depuis la sélection de cette option)",
+ "Members only (since they were invited)": "Seulement les membres (depuis leur invitation)",
+ "Members only (since they joined)": "Seulement les membres (depuis leur arrivée)",
+ "New community ID (e.g. +foo:%(localDomain)s)": "Nouvel identifiant de communauté (par ex. +foo:%(localDomain)s)",
+ "Message removed by %(userId)s": "Message supprimé par %(userId)s",
+ "Message removed": "Message supprimé",
+ "An email has been sent to %(emailAddress)s": "Un e-mail a été envoyé à %(emailAddress)s",
+ "A text message has been sent to %(msisdn)s": "Un message a été envoyé à %(msisdn)s",
+ "Remove from community": "Supprimer de la communauté",
+ "Disinvite this user from community?": "Désinviter cet utilisateur de la communauté ?",
+ "Remove this user from community?": "Supprimer cet utilisateur de la communauté ?",
+ "Failed to withdraw invitation": "Échec de l'annulation de l'invitation",
+ "Failed to remove user from community": "Échec de la suppression de l'utilisateur de la communauté",
+ "Filter community members": "Filtrer les membres de la communauté",
+ "Filter community rooms": "Filtrer les salons de la communauté",
+ "Failed to remove room from community": "Échec de la suppression du salon de la communauté",
+ "Failed to remove '%(roomName)s' from %(groupId)s": "Échec de la suppression de \"%(roomName)s\" de %(groupId)s",
+ "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Voulez-vous vraiment supprimer \"%(roomName)s\" de %(groupId)s ?",
+ "Removing a room from the community will also remove it from the community page.": "Supprimer un salon de la communauté le supprimera aussi de la page de la communauté.",
+ "Remove this room from the community": "Supprimer ce salon de la communauté",
+ "Delete Widget": "Supprimer le widget",
+ "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Supprimer un widget le supprime pour tous les utilisateurs du salon. Voulez-vous vraiment supprimer ce widget ?",
+ "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
+ "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s ont rejoint le salon %(count)s fois",
+ "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s ont rejoint le salon",
+ "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s a rejoint le salon %(count)s fois",
+ "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s a rejoint le salon",
+ "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s sont partis %(count)s fois",
+ "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s sont partis",
+ "%(oneUser)sleft %(count)s times|other": "%(oneUser)s est parti %(count)s fois",
+ "%(oneUser)sleft %(count)s times|one": "%(oneUser)s est parti",
+ "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s ont rejoint le salon et en sont partis %(count)s fois",
+ "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s ont rejoint le salon et en sont partis",
+ "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s a rejoint le salon et en est parti %(count)s fois",
+ "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s a rejoint le salon et en est parti",
+ "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s sont partis et revenus %(count)s fois",
+ "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s sont partis et revenus",
+ "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s est parti et revenu %(count)s fois",
+ "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s est parti et revenu",
+ "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s ont décliné leur invitation %(count)s fois",
+ "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s ont décliné leur invitation",
+ "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s a décliné son invitation %(count)s fois",
+ "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s a décliné son invitation",
+ "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)s ont vu leur invitation révoquée %(count)s fois",
+ "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)s ont vu leur invitation révoquée",
+ "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)s a vu son invitation révoquée %(count)s fois",
+ "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)s a vu son invitation révoquée",
+ "were invited %(count)s times|other": "ont été invités %(count)s fois",
+ "were invited %(count)s times|one": "ont été invités",
+ "was invited %(count)s times|other": "a été invité %(count)s fois",
+ "was invited %(count)s times|one": "a été invité",
+ "were banned %(count)s times|other": "ont été bannis %(count)s fois",
+ "were banned %(count)s times|one": "ont été bannis",
+ "was banned %(count)s times|other": "a été banni %(count)s fois",
+ "was banned %(count)s times|one": "a été banni",
+ "were unbanned %(count)s times|other": "ont vu leur bannissement révoqué %(count)s fois",
+ "were unbanned %(count)s times|one": "ont vu leur bannissement révoqué",
+ "was unbanned %(count)s times|other": "a vu son bannissement révoqué %(count)s fois",
+ "was unbanned %(count)s times|one": "a vu son bannissement révoqué",
+ "were kicked %(count)s times|other": "ont été exclus %(count)s fois",
+ "were kicked %(count)s times|one": "ont été exclus",
+ "was kicked %(count)s times|other": "a été exclu %(count)s fois",
+ "was kicked %(count)s times|one": "a été exclu",
+ "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s ont changé de nom %(count)s fois",
+ "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s ont changé de nom",
+ "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s a changé de nom %(count)s fois",
+ "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s a changé de nom",
+ "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s ont changé d'avatar %(count)s fois",
+ "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s ont changé d'avatar",
+ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s a changé d'avatar %(count)s fois",
+ "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s a changé d'avatar",
+ "%(items)s and %(count)s others|other": "%(items)s et %(count)s autres",
+ "%(items)s and %(count)s others|one": "%(items)s et un autre",
+ "And %(count)s more...|other": "Et %(count)s autres...",
+ "Matrix ID": "Identifiant Matrix",
+ "Matrix Room ID": "Identifiant de salon Matrix",
+ "email address": "adresse e-mail",
+ "Try using one of the following valid address types: %(validTypesList)s.": "Essayez d'utiliser un des types d'adresse valide suivants : %(validTypesList)s.",
+ "You have entered an invalid address.": "L'adresse saisie n'est pas valide.",
+ "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Les identifiants de communauté ne peuvent contenir que les caractères a-z, 0-9 ou '=_-./'",
+ "Something went wrong whilst creating your community": "Une erreur est survenue lors de la création de votre communauté",
+ "Create Community": "Créer une communauté",
+ "Community Name": "Nom de la communauté",
+ "Community ID": "Identifiant de la communauté",
+ "example": "exemple",
+ "Advanced options": "Options avancées",
+ "Block users on other matrix homeservers from joining this room": "Empêcher les utilisateurs d'autres serveurs d'accueil Matrix de rejoindre ce salon",
+ "This setting cannot be changed later!": "Ce paramètre ne peut pas être changé plus tard !",
+ "Add rooms to the community summary": "Ajouter des salons au sommaire de la communauté",
+ "Which rooms would you like to add to this summary?": "Quels salons souhaitez-vous ajouter à ce sommaire ?",
+ "Add to summary": "Ajouter au sommaire",
+ "Failed to add the following rooms to the summary of %(groupId)s:": "Échec de l'ajout des salons suivants au sommaire de %(groupId)s :",
+ "Add a Room": "Ajouter un salon",
+ "Failed to remove the room from the summary of %(groupId)s": "Échec de la suppression du salon du sommaire de %(groupId)s",
+ "The room '%(roomName)s' could not be removed from the summary.": "Le salon \"%(roomName)s\" n'a pas pu être supprimé du sommaire.",
+ "Add users to the community summary": "Ajouter des utilisateurs au sommaire de la communauté",
+ "Who would you like to add to this summary?": "Qui souhaitez-vous ajouter à ce sommaire ?",
+ "Failed to add the following users to the summary of %(groupId)s:": "Échec de l'ajout des utilisateurs suivants au sommaire de %(groupId)s :",
+ "Add a User": "Ajouter un utilisateur",
+ "Failed to remove a user from the summary of %(groupId)s": "Échec de la suppression d'un utilisateur du sommaire de %(groupId)s",
+ "The user '%(displayName)s' could not be removed from the summary.": "L'utilisateur \"%(displayName)s\" n'a pas pu être supprimé du sommaire.",
+ "Failed to update community": "Échec de la mise à jour de la communauté",
+ "Unable to accept invite": "Impossible d'accepter l'invitation",
+ "Unable to reject invite": "Impossible de décliner l'invitation",
+ "Leave Community": "Quitter la communauté",
+ "Leave %(groupName)s?": "Quitter %(groupName)s ?",
+ "Leave": "Quitter",
+ "Unable to leave room": "Impossible de partir du salon",
+ "Community Settings": "Paramètres de la communauté",
+ "Add rooms to this community": "Ajouter des salons à cette communauté",
+ "%(inviter)s has invited you to join this community": "%(inviter)s vous a invité à rejoindre cette communauté",
+ "You are an administrator of this community": "Vous êtes un(e) administrateur(trice) de cette communauté",
+ "You are a member of this community": "Vous êtes un membre de cette communauté",
+ "Community Member Settings": "Paramètres de membre de la communauté",
+ "Publish this community on your profile": "Publier cette communauté sur votre profil",
+ "Long Description (HTML)": "Description longue (HTML)",
+ "Description": "Description",
+ "Community %(groupId)s not found": "Communauté %(groupId)s non trouvée",
+ "This Home server does not support communities": "Ce serveur d'accueil ne prend pas en charge les communautés",
+ "Failed to load %(groupId)s": "Échec du chargement de %(groupId)s",
+ "Your Communities": "Vos communautés",
+ "You're not currently a member of any communities.": "Vous n'ếtes actuellement membre d'aucune communauté.",
+ "Error whilst fetching joined communities": "Erreur lors de l'obtention des communautés rejointes",
+ "Create a new community": "Créer une nouvelle communauté",
+ "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Créez une communauté pour grouper des utilisateurs et des salons ! Construisez une page d'accueil personnalisée pour distinguer votre espace dans l'univers Matrix.",
+ "Join an existing community": "Rejoindre une communauté existante",
+ "To join an existing community you'll have to know its community identifier; this will look something like +example:matrix.org.": "Pour rejoindre une communauté existante, vous devrez connaître son identifiant. Cela ressemblera à +exemple:matrix.org.",
+ "There's no one else here! Would you like to invite others or stop warning about the empty room?": "Il n'y a personne d'autre ici ! Voulez-vous inviter d'autres personnes ou ne plus être notifié de ce salon vide ?",
+ "Disable Emoji suggestions while typing": "Désactiver les suggestions d'emojis lors de la saisie",
+ "Disable big emoji in chat": "Désactiver les gros emojis dans les discussions",
+ "Mirror local video feed": "Refléter le flux vidéo local",
+ "Light theme": "Thème clair",
+ "Dark theme": "Thème sombre",
+ "Ignored Users": "Utilisateurs ignorés",
+ "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Un e-mail a été envoyé à %(emailAddress)s. Après avoir suivi le lien présent dans celui-ci, cliquez ci-dessous.",
+ "Ignores a user, hiding their messages from you": "Ignore un utilisateur, en masquant ses messages",
+ "Stops ignoring a user, showing their messages going forward": "N'ignore plus un utilisateur, en affichant ses messages à partir de maintenant",
+ "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "La visibilité de \"%(roomName)s\" dans %(groupId)s n'a pas pu être mise à jour.",
+ "Visibility in Room List": "Visibilité dans la liste des salons",
+ "Visible to everyone": "Visible pour tout le monde",
+ "Only visible to community members": "Visible uniquement par les membres de la communauté",
+ "Community Invites": "Invitations de communauté",
+ "Notify the whole room": "Notifier tout le salon",
+ "Room Notification": "Notification du salon",
+ "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Ces salons sont affichés aux membres de la communauté sur la page de la communauté. Les membres de la communauté peuvent rejoindre ces salons en cliquant dessus.",
+ "
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": "
HTML pour votre page de communauté
\n
\n Utilisez la description longue pour présenter la communauté aux nouveaux membres\n ou pour diffuser des liens importants\n
\n
\n Vous pouvez même utiliser des balises \"img\"\n
\n",
+ "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!": "Votre communauté n'a pas de description longue, une page HTML à montrer aux membres de la communauté. Cliquez ici pour ouvrir les réglages et créez-la !",
+ "Show these rooms to non-members on the community page and room list?": "Afficher ces salons aux non-membres sur la page de communauté et la liste des salons ?"
}
diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
new file mode 100644
index 0000000000..c3a103c4e5
--- /dev/null
+++ b/src/i18n/strings/gl.json
@@ -0,0 +1,18 @@
+{
+ "This email address is already in use": "Este enderezo de correo xa está a ser utilizado",
+ "This phone number is already in use": "Este número de teléfono xa está a ser utilizado",
+ "Failed to verify email address: make sure you clicked the link in the email": "Fallo na verificación do enderezo de correo: asegúrese de ter picado na ligazón do correo",
+ "The remote side failed to pick up": "O interlocutor non respondeu",
+ "Unable to capture screen": "Non se puido pillar a pantalla",
+ "Existing Call": "Chamada existente",
+ "You are already in a call.": "Xa está nunha chamada.",
+ "VoIP is unsupported": "VoIP non admitida",
+ "You cannot place VoIP calls in this browser.": "Non pode establecer chamadas VoIP en este navegador.",
+ "You cannot place a call with yourself.": "Non pode chamarse a vostede mesma.",
+ "Conference calls are not supported in this client": "Non pode establecer chamadas de Reunión en este cliente",
+ "Conference calls are not supported in encrypted rooms": "Nas salas cifradas non se pode establecer Chamadas de Reunión",
+ "Warning!": "Aviso!",
+ "Conference calling is in development and may not be reliable.": "As chamadas de Reunión poderían non ser totalmente estables xa que están en desenvolvemento.",
+ "Failed to set up conference call": "Fallo ao establecer a chamada de reunión",
+ "Conference call failed.": "Fallo na chamada de reunión."
+}
diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 1af47c90dd..b316e994a1 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -312,7 +312,7 @@
"Reason: %(reasonText)s": "Ok: %(reasonText)s",
"Revoke Moderator": "Moderátor visszahívása",
"Refer a friend to Riot:": "Ismerős meghívása a Riotba:",
- "Register": "Regisztráció",
+ "Register": "Regisztrál",
"%(targetName)s rejected the invitation.": "%(targetName)s elutasította a meghívót.",
"Reject invitation": "Meghívó elutasítása",
"Rejoin": "Újracsatlakozás",
@@ -799,7 +799,7 @@
"To ban users, you must be a": "Felhasználó kizárásához ilyen szinten kell lenned:",
"To remove other users' messages, you must be a": "Más üzenetének a törléséhez ilyen szinten kell lenned:",
"To send events of type , you must be a": " esemény küldéséhez ilyen szinten kell lenned:",
- "To change the room's avatar, you must be a": "A szoba avatar-jának a megváltoztatásához ilyen szinten kell lenned:",
+ "To change the room's avatar, you must be a": "A szoba avatarjának a megváltoztatásához ilyen szinten kell lenned:",
"To change the room's name, you must be a": "A szoba nevének megváltoztatásához ilyen szinten kell lenned:",
"To change the room's main address, you must be a": "A szoba elsődleges címének a megváltoztatásához ilyen szinten kell lenned:",
"To change the room's history visibility, you must be a": "A szoba naplója elérhetőségének a megváltoztatásához ilyen szinten kell lenned:",
@@ -902,5 +902,104 @@
"To join an existing community you'll have to know its community identifier; this will look something like +example:matrix.org.": "Ahhoz hogy csatlakozni tudj egy meglévő közösséghez ismerned kell a közösségi azonosítót ami például így nézhet ki: +pelda:matrix.org.",
"example": "példa",
"Failed to load %(groupId)s": "Nem sikerült betölteni: %(groupId)s",
- "Your Communities": "Közösségeid"
+ "Your Communities": "Közösségeid",
+ "You're not currently a member of any communities.": "Nem vagy tagja egyetlen közösségnek sem.",
+ "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Készíts közösséget hogy egybegyűjtsd a felhasználókat és szobákat! Készíts egy saját kezdőlapot amivel meghatározhatod magad a Matrix univerzumában.",
+ "%(names)s and %(count)s others are typing|other": "%(names)s és még %(count)s felhasználó gépel",
+ "And %(count)s more...|other": "És még %(count)s...",
+ "Something went wrong whilst creating your community": "Valami nem sikerült a közösség létrehozásánál",
+ "Mention": "Említ",
+ "Invite": "Meghív",
+ "Message removed": "Üzenet eltávolítva",
+ "Remove this room from the community": "A szoba törlése a közösségből",
+ "Delete Widget": "Kisalkalmazás törlése",
+ "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "A kisalkalmazás törlése minden felhasználót érint a szobában. Tényleg törölni szeretnéd?",
+ "Mirror local video feed": "Helyi videó folyam tükrözése",
+ "Failed to withdraw invitation": "Nem sikerült visszavonni a meghívót",
+ "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "A közösségi azonosítók csak az alábbi karaktereket tartalmazhatják: a-z, 0-9 vagy '=_-./'",
+ "%(names)s and %(count)s others are typing|one": "%(names)s és más ír",
+ "%(senderName)s sent an image": "%(senderName)s küldött egy képet",
+ "%(senderName)s sent a video": "%(senderName)s küldött egy videót",
+ "%(senderName)s uploaded a file": "%(senderName)s feltöltött egy fájlt",
+ "Disinvite this user?": "Visszavonod a felhasználó meghívását?",
+ "Kick this user?": "Kirúgod a felhasználót?",
+ "Unban this user?": "Visszaengeded a felhasználót?",
+ "Ban this user?": "Kitiltod a felhasználót?",
+ "Drop here to favourite": "Kedvencnek jelöléshez ejtsd ide",
+ "Drop here to tag direct chat": "Közvetlen csevegéshez való megjelöléshez ejtsd ide",
+ "Drop here to restore": "Visszaállításhoz ejtsd ide",
+ "Drop here to demote": "Lefokozáshoz ejtsd ide",
+ "You have been kicked from this room by %(userName)s.": "%(userName)s kirúgott ebből a szobából.",
+ "You have been banned from this room by %(userName)s.": "%(userName)s kitiltott ebből a szobából.",
+ "You are trying to access a room.": "Megpróbálod elérni ezt a szobát.",
+ "Members only (since the point in time of selecting this option)": "Csak tagok számára (a beállítás kiválasztásától)",
+ "Members only (since they were invited)": "Csak tagoknak (a meghívásuk idejétől)",
+ "Members only (since they joined)": "Csak tagoknak (amióta csatlakoztak)",
+ "An email has been sent to %(emailAddress)s": "E-mail-t neki küldtünk: %(emailAddress)s",
+ "A text message has been sent to %(msisdn)s": "Szöveges üzenetet küldtünk neki: %(msisdn)s",
+ "Disinvite this user from community?": "Visszavonod a felhasználó meghívóját a közösségből?",
+ "Remove this user from community?": "Eltávolítod a felhasználót a közösségből?",
+ "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
+ "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s %(count)s alkalommal csatlakozott",
+ "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s csatlakozott",
+ "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s %(count)s alkalommal csatlakozott",
+ "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s csatlakozott",
+ "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s %(count)s alkalommal távozott",
+ "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s távozott",
+ "%(oneUser)sleft %(count)s times|other": "%(oneUser)s %(count)s alkalommal távozott",
+ "%(oneUser)sleft %(count)s times|one": "%(oneUser)s távozott",
+ "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s %(count)s alkalommal csatlakozott és távozott",
+ "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s csatlakozott és távozott",
+ "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s %(count)s alkalommal csatlakozott és távozott",
+ "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s csatlakozott és távozott",
+ "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s %(count)s alkalommal távozott és újra csatlakozott",
+ "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s távozott és újra csatlakozott",
+ "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s %(count)s alkalommal távozott és újra csatlakozott",
+ "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s távozott és újra csatlakozott",
+ "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s %(count)s alkalommal elutasította a meghívóit",
+ "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s elutasította a meghívóit",
+ "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s %(count)s alkalommal elutasította a meghívóit",
+ "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s elutasította a meghívóit",
+ "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)s meghívóit %(count)s alkalommal visszavonták",
+ "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)s visszavonták a meghívóit",
+ "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)s meghívóit %(count)s alkalommal vonták vissza",
+ "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)s meghívóit visszavonták",
+ "were invited %(count)s times|other": "%(count)s alkalommal lett meghívva",
+ "were invited %(count)s times|one": "meg lett hívva",
+ "was invited %(count)s times|other": "%(count)s alkalommal lett meghívva",
+ "was invited %(count)s times|one": "meg lett hívva",
+ "were banned %(count)s times|other": "%(count)s alkalommal lett kitiltva",
+ "were banned %(count)s times|one": "lett kitiltva",
+ "was banned %(count)s times|other": "%(count)s alkalommal lett kitiltva",
+ "was banned %(count)s times|one": "ki lett tiltva",
+ "were unbanned %(count)s times|other": "%(count)s alkalommal lett visszaengedve",
+ "were unbanned %(count)s times|one": "vissza lett engedve",
+ "was unbanned %(count)s times|other": "%(count)s alkalommal lett visszaengedve",
+ "was unbanned %(count)s times|one": "vissza lett engedve",
+ "were kicked %(count)s times|other": "%(count)s alkalommal lett kirúgva",
+ "were kicked %(count)s times|one": "ki lett rúgva",
+ "was kicked %(count)s times|other": "%(count)s alkalommal ki lett rúgva",
+ "was kicked %(count)s times|one": "ki lett rúgva",
+ "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s %(count)s alkalommal megváltoztatta a nevét",
+ "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s megváltoztatta a nevét",
+ "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s %(count)s alkalommal megváltoztatta a nevét",
+ "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s megváltoztatta a nevét",
+ "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s %(count)s alkalommal megváltoztatta az avatarját",
+ "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s megváltoztatta az avatarját",
+ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s %(count)s alkalommal megváltoztatta az avatarját",
+ "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s megváltoztatta az avatarját",
+ "%(items)s and %(count)s others|other": "%(items)s és még %(count)s másik",
+ "%(items)s and %(count)s others|one": "%(items)s és még egy másik",
+ "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Az e-mail leküldésre került ide: %(emailAddress)s. Ha követte a levélben lévő linket kattints alább.",
+ "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "%(roomName)s szoba láthatóságát nem lehet frissíteni ebben a közösségben: %(groupId)s",
+ "Visibility in Room List": "Láthatóság a szoba listában",
+ "Visible to everyone": "Mindenki számára látható",
+ "Only visible to community members": "Csak a közösség számára látható",
+ "Community Invites": "Közösségi meghívók",
+ "
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": "
HTML a közösségi oldalhoz
\n
\n Használj hosszú leírást az tagok közösségbe való bemutatásához vagy terjessz\n hasznos linkeket\n
\n
\n Még 'img' tagokat is használhatsz\n
\n",
+ "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Ezek a szobák megjelennek a közösség tagjainak a közösségi oldalon. A közösség tagjai kattintással csatlakozhatnak a szobákhoz.",
+ "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!": "A közösségednek nincs bő leírása, HTML oldala ami megjelenik a közösség tagjainak. A létrehozáshoz kattints ide!",
+ "Notify the whole room": "Az egész szoba értesítése",
+ "Room Notification": "Szoba értesítések",
+ "Show these rooms to non-members on the community page and room list?": "Mutassuk meg ezeket a szobákat kívülállóknak a közösségi oldalon és a szobák listájában?"
}
diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index d4ffd6d4a3..1bc002f5f4 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -46,5 +46,6 @@
"Authentication": "Autenticazione",
"Alias (optional)": "Alias (opzionale)",
"Add a widget": "Aggiungi un widget",
- "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Un messaggio di testo è stato inviato a +%(msisdn)s. Inserisci il codice di verifica che contiene"
+ "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Un messaggio di testo è stato inviato a +%(msisdn)s. Inserisci il codice di verifica che contiene",
+ "Edit": "Modifica"
}
diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index 8e7857b2a6..4e7aa2fd55 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -110,7 +110,7 @@
"Active call (%(roomName)s)": "Aktywne połączenie (%(roomName)s)",
"Add email address": "Dodaj adres e-mail",
"Admin": "Administrator",
- "Admin Tools": "Narzędzia administracyjne",
+ "Admin Tools": "Narzędzia Administracyjne",
"VoIP": "VoIP (połączenie głosowe)",
"No Microphones detected": "Nie wykryto żadnego mikrofonu",
"No Webcams detected": "Nie wykryto żadnej kamerki internetowej",
@@ -212,7 +212,7 @@
"Drop File Here": "Upuść plik tutaj",
"Drop here to tag %(section)s": "Upuść tutaj by oznaczyć %(section)s",
"Ed25519 fingerprint": "Odcisk Ed25519",
- "Edit": "Edytuj",
+ "Edit": "Edycja",
"Email": "E-mail",
"Email address": "Adres e-mail",
"Email address (optional)": "Adres e-mail (opcjonalnie)",
@@ -386,7 +386,7 @@
"Revoke Moderator": "Usuń prawa moderatorskie",
"Revoke widget access": "Usuń dostęp do widżetów",
"Refer a friend to Riot:": "Zaproś znajomego do Riota:",
- "Register": "Zarejestruj",
+ "Register": "Rejestracja",
"%(targetName)s rejected the invitation.": "%(targetName)s odrzucił zaproszenie.",
"Reject invitation": "Odrzuć zaproszenie",
"Rejoin": "Dołącz ponownie",
@@ -773,5 +773,11 @@
"%(widgetName)s widget added by %(senderName)s": "Widżet %(widgetName)s został dodany przez %(senderName)s",
"%(widgetName)s widget removed by %(senderName)s": "Widżet %(widgetName)s został usunięty przez %(senderName)s",
"%(widgetName)s widget modified by %(senderName)s": "Widżet %(widgetName)s został zmodyfikowany przez %(senderName)s",
- "Robot check is currently unavailable on desktop - please use a web browser": "Sprawdzanie człowieczeństwa jest obecnie niedostępne na aplikacji klienckiej desktop - proszę użyć przeglądarki internetowej"
+ "Robot check is currently unavailable on desktop - please use a web browser": "Sprawdzanie człowieczeństwa jest obecnie niedostępne na aplikacji klienckiej desktop - proszę użyć przeglądarki internetowej",
+ "Unpin Message": "Odepnij Wiadomość",
+ "Add rooms to this community": "Dodaj pokoje do tej społeczności",
+ "Invite to Community": "Zaproszenie do Społeczności",
+ "Which rooms would you like to add to this community?": "Które pokoje chcesz dodać do tej społeczności?",
+ "Room name or alias": "Nazwa pokoju lub alias",
+ "Add to community": "Dodaj do społeczności"
}
diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index de9aa13c77..8ca50ec72f 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -8,7 +8,7 @@
"An email has been sent to": "Email был отправлен",
"A new password must be entered.": "Введите новый пароль.",
"Anyone who knows the room's link, apart from guests": "Любой, кто знает ссылку на комнату, кроме гостей",
- "Anyone who knows the room's link, including guests": "Любой, кто знает ссылку комнаты, включая гостей",
+ "Anyone who knows the room's link, including guests": "Любой, кто знает ссылку на комнату, включая гостей",
"Are you sure you want to reject the invitation?": "Вы уверены что вы хотите отклонить приглашение?",
"Are you sure you want to upload the following files?": "Вы уверены что вы хотите отправить следующие файлы?",
"Banned users": "Заблокированные пользователи",
@@ -62,7 +62,7 @@
"Filter room members": "Фильтр участников комнаты",
"Forget room": "Забыть комнату",
"Forgot your password?": "Забыли пароль?",
- "For security, this session has been signed out. Please sign in again.": "По соображениям безопасности, эта сессия была прекращена. Пожалуйста, войдите снова.",
+ "For security, this session has been signed out. Please sign in again.": "Для обеспечения безопасности ваша сессия была завершена. Пожалуйста, войдите снова.",
"Found a bug?": "Нашли ошибку?",
"Hangup": "Закончить",
"Historical": "Архив",
@@ -74,7 +74,7 @@
"Invite new room members": "Пригласить новых участников в комнату",
"Invites": "Приглашает",
"Invites user with given id to current room": "Приглашает пользователя с заданным ID в текущую комнату",
- "Sign in with": "Войти с помощью",
+ "Sign in with": "Войти, используя",
"Joins room with given alias": "Входит в комнату с заданным псевдонимом",
"Kicks user with given id": "Выкидывает пользователя с заданным ID",
"Labs": "Лаборатория",
@@ -183,11 +183,11 @@
"%(targetName)s joined the room.": "%(targetName)s вошел(ла) в комнату.",
"%(senderName)s kicked %(targetName)s.": "%(senderName)s выкинул %(targetName)s.",
"%(targetName)s left the room.": "%(targetName)s покинул комнату.",
- "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(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.": "%(senderName)s сделал будущую историю комнаты видимой все участники комнаты.",
- "%(senderName)s made future room history visible to anyone.": "%(senderName)s сделал будущую историю комнаты видимой любой.",
- "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s сделал будущую историю комнаты видимой неизвестный (%(visibility)s).",
+ "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(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.": "%(senderName)s сделал(а) историю комнаты видимой для всех участников комнаты.",
+ "%(senderName)s made future room history visible to anyone.": "%(senderName)s сделал(а) историю комнаты видимой для всех.",
+ "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s сделал(а) историю комнаты видимой для неизвестного (%(visibility)s).",
"Missing room_id in request": "Отсутствует room_id в запросе",
"Missing user_id in request": "Отсутствует user_id в запросе",
"Must be viewing a room": "Необходимо посмотреть комнату",
@@ -306,14 +306,14 @@
"'%(alias)s' is not a valid format for an alias": "'%(alias)s' недопустимый формат псевдонима",
"Join Room": "Войти в комнату",
"Kick": "Выгнать",
- "Local addresses for this room:": "Локальный адрес этой комнаты:",
+ "Local addresses for this room:": "Локальные адреса этой комнаты:",
"Markdown is disabled": "Markdown отключен",
"Markdown is enabled": "Markdown включен",
"matrix-react-sdk version:": "версия matrix-react-sdk:",
"New address (e.g. #foo:%(localDomain)s)": "Новый адрес (например, #foo:%(localDomain)s)",
"New passwords don't match": "Новые пароли не совпадают",
"not set": "не задано",
- "not specified": "не определено",
+ "not specified": "не определен",
"No devices with registered encryption keys": "Нет устройств с зарегистрированными ключами шифрования",
"No more results": "Больше никаких результатов",
"No results": "Нет результатов",
@@ -334,7 +334,7 @@
"Report it": "Сообщить об этом",
"Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Сброс пароля на данный момент сбрасывает ключи шифрования на всех устройствах, делая зашифрованную историю чатов нечитаемой. Чтобы избежать этого, экспортируйте ключи комнат и импортируйте их после сброса пароля. В будущем это будет исправлено.",
"Return to app": "Вернуться в приложение",
- "Riot does not have permission to send you notifications - please check your browser settings": "Riot не имеет разрешение на отправку уведомлений, проверьте параметры своего браузера",
+ "Riot does not have permission to send you notifications - please check your browser settings": "У Riot нет разрешений на отправку уведомлений - проверьте настройки браузера",
"Riot was not given permission to send notifications - please try again": "Riot не получил разрешение на отправку уведомлений, пожалуйста, попробуйте снова",
"riot-web version:": "версия riot-web:",
"Room %(roomId)s not visible": "Комната %(roomId)s невидима",
@@ -509,7 +509,7 @@
"Session ID": "ID сессии",
"%(senderName)s set a profile picture.": "%(senderName)s установил изображение профиля.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s изменил отображаемое имя на %(displayName)s.",
- "Signed Out": "Вышли",
+ "Signed Out": "Выполнен выход",
"Sorry, this homeserver is using a login which is not recognised ": "К сожалению, этот домашний сервер использует неизвестный метод авторизации ",
"Tagged as: ": "Теги: ",
"The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Предоставленный ключ подписи соответствует ключу, полученному от %(userId)s с устройства %(deviceId)s. Устройство помечено как проверенное.",
@@ -586,7 +586,7 @@
"ex. @bob:example.com": "например @bob:example.com",
"Add User": "Добавить пользователя",
"This Home Server would like to make sure you are not a robot": "Этот домашний сервер хочет убедиться, что вы не робот",
- "Sign in with CAS": "Войти с помощью CAS",
+ "Sign in with CAS": "Войти, используя CAS",
"You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "Вы можете использовать настраиваемые параметры сервера для входа на другие серверы Matrix, указав другой URL-адрес домашнего сервера.",
"This allows you to use this app with an existing Matrix account on a different home server.": "Это позволяет использовать это приложение с существующей учетной записью Matrix на другом домашнем сервере.",
"You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "Вы также можете установить другой сервер идентификации, но это, как правило, будет препятствовать взаимодействию с пользователями на основе адреса email.",
@@ -612,7 +612,7 @@
"Disable URL previews by default for participants in this room": "Отключить предпросмотр URL-адресов по умолчанию для участников этой комнаты",
"URL previews are %(globalDisableUrlPreview)s by default for participants in this room.": "Предварительный просмотр URL-адресов %(globalDisableUrlPreview)s по умолчанию для участников этой комнаты.",
"URL Previews": "Предварительный просмотр URL-адресов",
- "Enable URL previews for this room (affects only you)": "Включить предпросмотр URL-адресов для этой комнаты (влияет только на вас)",
+ "Enable URL previews for this room (affects only you)": "Включить предпросмотр URL-адресов для этой комнаты (касается только вас)",
"Drop file here to upload": "Перетащите файл сюда для отправки",
" (unsupported)": " (не поддерживается)",
"Ongoing conference call%(supportedText)s.": "Установлен групповой вызов %(supportedText)s.",
@@ -623,7 +623,7 @@
"Online": "В сети",
"Idle": "Неактивен",
"Offline": "Не в сети",
- "Disable URL previews for this room (affects only you)": "Отключить предпросмотр URL-адресов для этой комнаты (влияет только на вас)",
+ "Disable URL previews for this room (affects only you)": "Отключить предпросмотр URL-адресов для этой комнаты (касается только вас)",
"%(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",
@@ -740,10 +740,10 @@
"Delete widget": "Удалить виджет",
"Define the power level of a user": "Определить уровень доступа пользователя",
"Do you want to load widget from URL:": "Загрузить виджет из URL-адреса:",
- "Edit": "Изменить",
+ "Edit": "Редактировать",
"Enable automatic language detection for syntax highlighting": "Включить автоматическое определение языка для подсветки синтаксиса",
"Hide Apps": "Скрыть приложения",
- "Hide join/leave messages (invites/kicks/bans unaffected)": "Скрыть сообщения о входе/выходе (приглашениях/выкидываниях/банах)",
+ "Hide join/leave messages (invites/kicks/bans unaffected)": "Скрыть сообщения о входе/выходе (не применяется к приглашениям/выкидываниям/банам)",
"Hide avatar and display name changes": "Скрыть сообщения об изменении аватаров и отображаемых имен",
"Integrations Error": "Ошибка интеграции",
"AM": "AM",
@@ -857,5 +857,134 @@
"Community Member Settings": "Настройки участников сообщества",
"Publish this community on your profile": "Опубликовать это сообщество в вашем профиле",
"Long Description (HTML)": "Длинное описание (HTML)",
- "Community Settings": "Настройки сообщества"
+ "Community Settings": "Настройки сообщества",
+ "Invite to Community": "Пригласить в сообщество",
+ "Add to community": "Добавить в сообщество",
+ "Add rooms to the community": "Добавление комнат в сообщество",
+ "Which rooms would you like to add to this community?": "Какие комнаты вы хотите добавить в это сообщество?",
+ "Who would you like to add to this community?": "Кого бы вы хотели добавить в это сообщество?",
+ "Invite new community members": "Пригласить новых членов сообщества",
+ "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Предупреждение: любой, кого вы добавляете в сообщество, будет виден всем, кто знает ID сообщества",
+ "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID": "Предупреждение: любая комната, добавляемая в сообщество, будет видна всем, кто знает ID сообщества",
+ "Add rooms to this community": "Добавить комнаты в это сообщество",
+ "Your community invitations have been sent.": "Ваши приглашения в сообщество были отправлены.",
+ "Failed to invite users to community": "Не удалось пригласить пользователей в сообщество",
+ "Communities": "Сообщества",
+ "Invalid community ID": "Недопустимый ID сообщества",
+ "'%(groupId)s' is not a valid community ID": "'%(groupId)s' - недействительный ID сообщества",
+ "Related Communities": "Связанные сообщества",
+ "Related communities for this room:": "Связанные сообщества для этой комнаты:",
+ "This room has no related communities": "Эта комната не имеет связанных сообществ",
+ "New community ID (e.g. +foo:%(localDomain)s)": "Новый ID сообщества (напр. +foo:%(localDomain)s)",
+ "Remove from community": "Удалить из сообщества",
+ "Failed to remove user from community": "Не удалось удалить пользователя из сообщества",
+ "Filter community members": "Фильтр участников сообщества",
+ "Filter community rooms": "Фильтр комнат сообщества",
+ "Failed to remove room from community": "Не удалось удалить комнату из сообщества",
+ "Removing a room from the community will also remove it from the community page.": "Удаление комнаты из сообщества также удалит ее со страницы сообщества.",
+ "Community IDs may only contain alphanumeric characters": "ID сообщества могут содержать только буквенно-цифровые символы",
+ "Create Community": "Создать сообщество",
+ "Community Name": "Имя сообщества",
+ "Community ID": "ID сообщества",
+ "example": "пример",
+ "Add rooms to the community summary": "Добавить комнаты в сводку сообщества",
+ "Add users to the community summary": "Добавить пользователей в сводку сообщества",
+ "Failed to update community": "Не удалось обновить сообщество",
+ "Leave Community": "Покинуть сообщество",
+ "%(inviter)s has invited you to join this community": "%(inviter)s пригласил(а) вас присоединиться к этому сообществу",
+ "You are a member of this community": "Вы являетесь участником этого сообщества",
+ "You are an administrator of this community": "Вы являетесь администратором этого сообщества",
+ "Community %(groupId)s not found": "Сообщество %(groupId)s не найдено",
+ "This Home server does not support communities": "Этот домашний сервер не поддерживает сообщества",
+ "Failed to load %(groupId)s": "Ошибка загрузки %(groupId)s",
+ "Your Communities": "Ваши сообщества",
+ "You're not currently a member of any communities.": "В настоящее время вы не являетесь членом каких-либо сообществ.",
+ "Error whilst fetching joined communities": "Ошибка при загрузке сообществ",
+ "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.": "Создайте сообщество для объединения пользователей и комнат! Создайте собственную домашнюю страницу, чтобы выделить свое пространство во вселенной Matrix.",
+ "Join an existing community": "Присоединиться к существующему сообществу",
+ "To join an existing community you'll have to know its community identifier; this will look something like +example:matrix.org.": "Чтобы присоединиться к существующему сообществу, вам нужно знать его ID; это будет выглядеть примерно так+primer:matrix.org.",
+ "Something went wrong whilst creating your community": "При создании сообщества что-то пошло не так",
+ "%(names)s and %(count)s others are typing|other": "%(names)s и %(count)s другие печатают",
+ "And %(count)s more...|other": "И более %(count)s...",
+ "Delete Widget": "Удалить виджет",
+ "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Удаление виджета удаляет его для всех пользователей этой комнаты. Вы действительно хотите удалить этот виджет?",
+ "Message removed": "Сообщение удалено",
+ "Mirror local video feed": "Зеркальное отображение видео",
+ "Invite": "Пригласить",
+ "Remove this room from the community": "Удалить эту комнату из сообщества",
+ "Mention": "Упоминание",
+ "Failed to withdraw invitation": "Не удалось отозвать приглашение",
+ "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "ID сообществ могут содержать только символы a-z, 0-9, или '=_-./'",
+ "%(names)s and %(count)s others are typing|one": "%(names)s и еще кто-то печатает",
+ "%(senderName)s sent an image": "%(senderName)s отправил(а) изображение",
+ "%(senderName)s sent a video": "%(senderName)s отправил(а) видео",
+ "%(senderName)s uploaded a file": "%(senderName)s загрузил(а) файл",
+ "Disinvite this user?": "Отменить приглашение этого пользователя?",
+ "Kick this user?": "Выгнать этого пользователя?",
+ "Unban this user?": "Разблокировать этого пользователя?",
+ "Ban this user?": "Заблокировать этого пользователя?",
+ "Drop here to favourite": "Перетащите сюда для добавления в избранные",
+ "You have been kicked from this room by %(userName)s.": "%(userName)s выгнал(а) вас из этой комнаты.",
+ "You have been banned from this room by %(userName)s.": "%(userName)s заблокировал(а) вас в этой комнате.",
+ "You are trying to access a room.": "Вы пытаетесь получить доступ к комнате.",
+ "Members only (since the point in time of selecting this option)": "Только участники (с момента выбора этого параметра)",
+ "Members only (since they were invited)": "Только участники (с момента их приглашения)",
+ "Members only (since they joined)": "Только участники (с момента их присоединения)",
+ "An email has been sent to %(emailAddress)s": "Письмо было отправлено на %(emailAddress)s",
+ "A text message has been sent to %(msisdn)s": "Текстовое сообщение отправлено на %(msisdn)s",
+ "Disinvite this user from community?": "Отозвать приглашение этого пользователя в сообщество?",
+ "Remove this user from community?": "Удалить этого пользователя из сообщества?",
+ "%(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 присоединились",
+ "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s присоединился(-лась) %(count)s раз",
+ "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s присоединился(-лась)",
+ "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s покинули %(count)s раз",
+ "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s покинули",
+ "%(oneUser)sleft %(count)s times|other": "%(oneUser)sl покинул(а) %(count)s раз",
+ "%(oneUser)sleft %(count)s times|one": "%(oneUser)s покинул(а)",
+ "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s присоединились и покинули %(count)s раз",
+ "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s присоединились и покинули",
+ "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s присоединился(-лась) и покинул(а) %(count)s раз",
+ "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s присоединился(-лась) и покинул(а)",
+ "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s покинули и снова присоединились %(count)s раз",
+ "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s покинули и снова присоединились",
+ "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s покинул(а) и снова присоединился(-лась) %(count)s раз",
+ "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s покинул(а) и снова присоединился(-лась)",
+ "were invited %(count)s times|other": "были приглашены %(count)s раз",
+ "were invited %(count)s times|one": "были приглашены",
+ "was invited %(count)s times|other": "был(а) приглашен(а) %(count)s раз",
+ "was invited %(count)s times|one": "был(а) приглашен(а)",
+ "were banned %(count)s times|other": "были заблокированы %(count)s раз",
+ "were banned %(count)s times|one": "были заблокированы",
+ "was banned %(count)s times|other": "был(а) заблокирован(а) %(count)s раз",
+ "was banned %(count)s times|one": "был(а) заблокирован(а)",
+ "were unbanned %(count)s times|other": "были разблокированы %(count)s раз",
+ "were unbanned %(count)s times|one": "были разблокированы",
+ "was unbanned %(count)s times|other": "был(а) разблокирован(а) %(count)s раз",
+ "was unbanned %(count)s times|one": "был(а) разблокирован(а)",
+ "were kicked %(count)s times|other": "были выкинуты %(count)s раз",
+ "were kicked %(count)s times|one": "были выкинуты",
+ "was kicked %(count)s times|other": "был(а) выкинут(а) %(count)s раз",
+ "was kicked %(count)s times|one": "был(а) выкинут(а)",
+ "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s изменили свое имя %(count)s раз",
+ "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s изменили свое имя",
+ "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s изменил(а) свое имя %(count)s раз",
+ "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s изменил(а) свое имя",
+ "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s изменили свои аватары %(count)s раз",
+ "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s изменили свои аватары",
+ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s изменил(а) свой аватар %(count)s раз",
+ "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s изменил(а) свой аватар",
+ "%(items)s and %(count)s others|other": "%(items)s и %(count)s других",
+ "%(items)s and %(count)s others|one": "%(items)s и один другой",
+ "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Сообщение отправлено на %(emailAddress)s. После перехода по ссылке в отправленном вам письме, щелкните ниже.",
+ "Room Notification": "Уведомления комнаты",
+ "Drop here to tag direct chat": "Перетащите сюда, чтобы отметить как прямой чат",
+ "Drop here to restore": "Перетащиет сюда для восстановления",
+ "Drop here to demote": "Перетащите сюда для понижения",
+ "Community Invites": "Приглашения в сообщества",
+ "Notify the whole room": "Уведомить всю комнату",
+ "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Эти комнаты отображаются для участников сообщества на странице сообщества. Участники сообщества могут присоединиться к комнатам, щелкнув на них.",
+ "Show these rooms to non-members on the community page and room list?": "Следует ли показывать эти комнаты посторонним на странице сообщества и в комнате?"
}
diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json
new file mode 100644
index 0000000000..837a55e380
--- /dev/null
+++ b/src/i18n/strings/sk.json
@@ -0,0 +1,930 @@
+{
+ "This email address is already in use": "Táto emailová adresa sa už používa",
+ "This phone number is already in use": "Toto telefónne číslo sa už používa",
+ "Failed to verify email address: make sure you clicked the link in the email": "Nepodarilo sa overiť emailovú adresu: Uistite sa, že ste správne klikli na odkaz v emailovej správe",
+ "Call Timeout": "Časový limit hovoru",
+ "The remote side failed to pick up": "Vzdialenej strane sa nepodarilo priať hovor",
+ "Unable to capture screen": "Nie je možné zachytiť obrazovku",
+ "Existing Call": "Existujúci hovor",
+ "You are already in a call.": "Už ste súčasťou iného hovoru.",
+ "VoIP is unsupported": "VoIP nie je podporovaný",
+ "You cannot place VoIP calls in this browser.": "Použitím tohoto webového prehliadača nemôžete uskutočniť hovory.",
+ "You cannot place a call with yourself.": "Nemôžete uskutočniť hovor so samým sebou.",
+ "Conference calls are not supported in this client": "Tento klient nepodporuje konferenčné hovory",
+ "Conference calls are not supported in encrypted rooms": "Konferenčné hovory nie sú podporované v šifrovaných miestnostiach",
+ "Warning!": "Upozornenie!",
+ "Conference calling is in development and may not be reliable.": "Konferenčné hovory sú stále vo vývoji a nemusia byť úplne spoľahlivé.",
+ "Failed to set up conference call": "Nepodarilo sa nastaviť konferenčný hovor",
+ "Conference call failed.": "Konferenčný hovor sa nepodarilo uskutočniť.",
+ "The file '%(fileName)s' failed to upload": "Nepodarilo sa nahrať súbor '%(fileName)s'",
+ "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Veľkosť súboru '%(fileName)s' prekračuje limit veľkosti súboru nahrávania na tento domovský server",
+ "Upload Failed": "Nahrávanie zlyhalo",
+ "Sun": "Ne",
+ "Mon": "Po",
+ "Tue": "Ut",
+ "Wed": "St",
+ "Thu": "Št",
+ "Fri": "Pi",
+ "Sat": "So",
+ "Jan": "Jan",
+ "Feb": "Feb",
+ "Mar": "Mar",
+ "Apr": "Apr",
+ "May": "Maj",
+ "Jun": "Jun",
+ "Jul": "Jul",
+ "Aug": "Aug",
+ "Sep": "Sep",
+ "Oct": "Okt",
+ "Nov": "Nov",
+ "Dec": "Dec",
+ "PM": "PM",
+ "AM": "AM",
+ "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
+ "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(time)s",
+ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s",
+ "Who would you like to add to this community?": "Koho si želáte pridať do tejto komunity?",
+ "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Pozor: Každá osoba, ktorú pridáte do komunity bude verejne dostupná pre všetkých, čo poznajú ID komunity",
+ "Invite new community members": "Pozvať nových členov komunity",
+ "Name or matrix ID": "Meno alebo matrix ID",
+ "Invite to Community": "Pozvať do komunity",
+ "Which rooms would you like to add to this community?": "Ktoré miestnosti by ste radi pridali do tejto komunity?",
+ "Add rooms to the community": "Pridať miestnosti do komunity",
+ "Room name or alias": "Názov miestnosti alebo alias",
+ "Add to community": "Pridať do komunity",
+ "Failed to invite the following users to %(groupId)s:": "Do komunity %(groupId)s sa nepodarilo pozvať nasledujúcich používateľov:",
+ "Failed to invite users to community": "Do komunity sa nepodarilo pozvať používateľov",
+ "Failed to invite users to %(groupId)s": "Do komunity %(groupId)s sa nepodarilo pozvať používateľov",
+ "Failed to add the following rooms to %(groupId)s:": "Do komunity %(groupId)s sa nepodarilo pridať nasledujúce miestnosti:",
+ "Riot does not have permission to send you notifications - please check your browser settings": "Riot nemá udelené povolenie, aby vám mohol posielať oznámenia - Prosím, skontrolujte nastavenia vašeho prehliadača",
+ "Riot was not given permission to send notifications - please try again": "Aplikácii Riot neboli udelené oprávnenia potrebné pre posielanie oznámení - prosím, skúste to znovu",
+ "Unable to enable Notifications": "Nie je možné povoliť oznámenia",
+ "This email address was not found": "Túto emailovú adresu sa nepodarilo nájsť",
+ "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Zdá sa, že vaša emailová adresa nie je priradená k žiadnemu Matrix ID na tomto domovskom servery.",
+ "Default": "Predvolené",
+ "User": "Používateľ",
+ "Moderator": "Moderátor",
+ "Admin": "Správca",
+ "Start a chat": "Začať konverzáciu",
+ "Who would you like to communicate with?": "S kým si želáte komunikovať?",
+ "Email, name or matrix ID": "Emailová adresa, meno alebo matrix ID",
+ "Start Chat": "Začať konverzáciu",
+ "Invite new room members": "Pozvať nových členov do miestnosti",
+ "Who would you like to add to this room?": "Koho si želáte pridať do tejto miestnosti?",
+ "Send Invites": "Poslať pozvánky",
+ "Failed to invite user": "Nepodarilo sa pozvať používateľa",
+ "Operation failed": "Operácia zlyhala",
+ "Failed to invite": "Pozvanie zlyhalo",
+ "Failed to invite the following users to the %(roomName)s room:": "Do miestnosti %(roomName)s sa nepodarilo pozvať nasledujúcich používateľov:",
+ "You need to be logged in.": "Mali by ste byť prihlásení.",
+ "You need to be able to invite users to do that.": "Na uskutočnenie tejto akcie by ste mali byť schopní pozývať používateľov.",
+ "Unable to create widget.": "Nie je možné vytvoriť widget.",
+ "Failed to send request.": "Nepodarilo sa odoslať požiadavku.",
+ "This room is not recognised.": "Nie je možné rozpoznať takúto miestnosť.",
+ "Power level must be positive integer.": "Úroveň moci musí byť kladné celé číslo.",
+ "You are not in this room.": "Nenachádzate sa v tejto miestnosti.",
+ "You do not have permission to do that in this room.": "V tejto miestnosti nemáte oprávnenie na vykonanie takejto akcie.",
+ "Missing room_id in request": "V požiadavke chýba room_id",
+ "Must be viewing a room": "Musí byť zobrazená miestnosť",
+ "Room %(roomId)s not visible": "Miestnosť %(roomId)s nie je viditeľná",
+ "Missing user_id in request": "V požiadavke chýba user_id",
+ "Failed to lookup current room": "Nepodarilo sa vyhľadať aktuálnu miestnosť",
+ "Usage": "Použitie",
+ "/ddg is not a command": "/ddg nie je žiaden príkaz",
+ "To use it, just wait for autocomplete results to load and tab through them.": "Ak to chcete použiť, len počkajte na načítanie výsledkov automatického dopĺňania a cyklicky prechádzajte stláčaním klávesu tab..",
+ "Unrecognised room alias:": "Nerozpoznaný alias miestnosti:",
+ "Ignored user": "Ignorovaný používateľ",
+ "You are now ignoring %(userId)s": "Od teraz ignorujete používateľa %(userId)s",
+ "Unignored user": "Ignorácia zrušená",
+ "You are no longer ignoring %(userId)s": "Od teraz viac neignorujete používateľa %(userId)s",
+ "Unknown (user, device) pair:": "Neznámy pár (používateľ, zariadenie):",
+ "Device already verified!": "Zariadenie už overené!",
+ "WARNING: Device already verified, but keys do NOT MATCH!": "POZOR: Zariadenie je už overené, ale kľúče SA NEZHODUJÚ!",
+ "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "POZOR: OVERENIE KĽÚČOV ZLYHALO! Podpisovací kľúč zo zariadenia %(deviceId)s používateľa %(userId)s je \"%(fprint)s\" čo sa nezhoduje s poskytnutým kľúčom \"%(fingerprint)s\". Mohlo by to znamenať, že vaša komunikácia je práve odpočúvaná!",
+ "Verified key": "Kľúč overený",
+ "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Podpisovací kľúč, ktorý ste poskytli súhlasí s podpisovacím kľúčom, ktorý ste dostali zo zariadenia %(deviceId)s používateľa %(userId)s's. Zariadenie je považované za overené.",
+ "Unrecognised command:": "Nerozpoznaný príkaz:",
+ "Reason": "Dôvod",
+ "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s prijal pozvanie pre %(displayName)s.",
+ "%(targetName)s accepted an invitation.": "%(targetName)s prijal pozvanie.",
+ "%(senderName)s requested a VoIP conference.": "%(senderName)s požiadal o VOIP konferenciu.",
+ "%(senderName)s invited %(targetName)s.": "%(senderName)s pozval %(targetName)s.",
+ "%(senderName)s banned %(targetName)s.": "%(senderName)s zakázal vstup %(targetName)s.",
+ "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.": "%(senderName)s si zmenil zobrazované meno z %(oldDisplayName)s na %(displayName)s.",
+ "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s si nastavil zobrazované meno %(displayName)s.",
+ "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s odstránil svoje zobrazované meno (%(oldDisplayName)s).",
+ "%(senderName)s removed their profile picture.": "%(senderName)s si z profilu odstránil obrázok.",
+ "%(senderName)s changed their profile picture.": "%(senderName)s si zmenil obrázok v profile.",
+ "%(senderName)s set a profile picture.": "%(senderName)s si nastavil obrázok v profile.",
+ "VoIP conference started.": "Začala VoIP konferencia.",
+ "%(targetName)s joined the room.": "%(targetName)s vstúpil do miestnosti.",
+ "VoIP conference finished.": "Skončila VoIP konferencia.",
+ "%(targetName)s rejected the invitation.": "%(targetName)s odmietol pozvanie.",
+ "%(targetName)s left the room.": "%(targetName)s opustil miestnosť.",
+ "%(senderName)s unbanned %(targetName)s.": "%(senderName)s povolil vstup %(targetName)s.",
+ "%(senderName)s kicked %(targetName)s.": "%(senderName)s vykopol %(targetName)s.",
+ "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s stiahol pozvanie %(targetName)s.",
+ "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s zmenil tému na \"%(topic)s\".",
+ "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s odstránil názov miestnosti.",
+ "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s zmenil názov miestnosti na %(roomName)s.",
+ "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s poslal obrázok.",
+ "Someone": "Niekto",
+ "(not supported by this browser)": "(Nepodporované v tomto prehliadači)",
+ "%(senderName)s answered the call.": "%(senderName)s prijal hovor.",
+ "(could not connect media)": "(nie je možné spojiť médiá)",
+ "(no answer)": "(žiadna odpoveď)",
+ "(unknown failure: %(reason)s)": "(neznáma chyba: %(reason)s)",
+ "%(senderName)s ended the call.": "%(senderName)s ukončil hovor.",
+ "%(senderName)s placed a %(callType)s call.": "%(senderName)s uskutočnil %(callType)s hovor.",
+ "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s pozval %(targetDisplayName)s vstúpiť do miestnosti.",
+ "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s sprístupnil budúcu históriu miestnosti pre všetkých členov, od kedy boli pozvaní.",
+ "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s sprístupnil budúcu históriu miestnosti pre všetkých členov, od kedy vstúpili.",
+ "%(senderName)s made future room history visible to all room members.": "%(senderName)s sprístupnil budúcu históriu miestnosti pre všetkých členov.",
+ "%(senderName)s made future room history visible to anyone.": "%(senderName)s sprístupnil budúcu históriu miestnosti pre všetkých.",
+ "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s sprístupnil budúcu históriu miestnosti neznámym (%(visibility)s).",
+ "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s povolil E2E šifrovanie (algoritmus %(algorithm)s).",
+ "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s z %(fromPowerLevel)s na %(toPowerLevel)s",
+ "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s zmenil úroveň moci pre %(powerLevelDiffText)s.",
+ "%(senderName)s changed the pinned messages for the room.": "%(senderName)s zmenil pripnuté správy pre túto miestnosť.",
+ "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s zmenil widget %(widgetName)s",
+ "%(widgetName)s widget added by %(senderName)s": "%(senderName)s pridal widget %(widgetName)s",
+ "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s odstránil widget %(widgetName)s",
+ "Communities": "Komunity",
+ "Message Pinning": "Pripnutie správ",
+ "%(displayName)s is typing": "%(displayName)s píše",
+ "%(names)s and %(count)s others are typing|other": "%(names)s a %(count)s ďalší píšu",
+ "%(names)s and %(count)s others are typing|one": "%(names)s a jeden ďalší píše",
+ "%(names)s and %(lastPerson)s are typing": "%(names)s a %(lastPerson)s píšu",
+ "Failure to create room": "Nepodarilo sa vytvoriť miestnosť",
+ "Server may be unavailable, overloaded, or you hit a bug.": "Server môže byť nedostupný, preťažený, alebo ste narazili na chybu.",
+ "Unnamed Room": "Nepomenovaná miestnosť",
+ "Your browser does not support the required cryptography extensions": "Váš prehliadač nepodporuje požadované kryptografické rozšírenia",
+ "Not a valid Riot keyfile": "Toto nie je správny súbor s kľúčami Riot",
+ "Authentication check failed: incorrect password?": "Kontrola overenia zlyhala: Nesprávne heslo?",
+ "Failed to join room": "Nepodarilo sa vstúpiť do miestnosti",
+ "Active call (%(roomName)s)": "Aktívny hovor (%(roomName)s)",
+ "unknown caller": "neznámeho volajúceho",
+ "Incoming voice call from %(name)s": "Prichádzajúci audio hovor od %(name)s",
+ "Incoming video call from %(name)s": "Prichádzajúci video hovor od %(name)s",
+ "Incoming call from %(name)s": "Prichádzajúci hovor od %(name)s",
+ "Decline": "Odmietnuť",
+ "Accept": "Prijať",
+ "Error": "Chyba",
+ "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Na číslo +%(msisdn)s bola odoslaná textová správa, ktorá obsahuje overovací kód. Prosím zadajte tento overovací kód",
+ "Incorrect verification code": "Nesprávny overovací kód",
+ "Enter Code": "Vložte kód",
+ "Submit": "Odoslať",
+ "Phone": "Telefón",
+ "Add phone number": "Pridať telefónne číslo",
+ "Add": "Pridať",
+ "Failed to upload profile picture!": "Do profilu sa nepodarilo nahrať obrázok!",
+ "Upload new:": "Nahrať nový:",
+ "No display name": "Žiadne zobrazované meno",
+ "New passwords don't match": "Nové heslá sa nezhodujú",
+ "Passwords can't be empty": "Heslá nemôžu byť prázdne",
+ "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Zmena hesla momentálne obnoví šifrovacie kľúče na všetkých vašich zariadeniach, čo spôsobí, že história vašich šifrovaných konverzácií sa stane nečitateľná, jedine že si pred zmenou hesla exportujete kľúče miestností do súboru a po zmene kľúče importujete naspäť. V budúcnosti bude táto funkcia vylepšená.",
+ "Continue": "Pokračovať",
+ "Export E2E room keys": "Exportovať E2E šifrovacie kľúče miestností",
+ "Do you want to set an email address?": "Želáte si nastaviť emailovú adresu?",
+ "Current password": "Súčasné heslo",
+ "Password": "Heslo",
+ "New Password": "Nové heslo",
+ "Confirm password": "Potvrdiť heslo",
+ "Change Password": "Zmeniť heslo",
+ "Your home server does not support device management.": "Zdá sa, že váš domovský server nepodporuje správu zariadení.",
+ "Unable to load device list": "Nie je možné načítať zoznam zariadení",
+ "Device ID": "ID zariadenia",
+ "Device Name": "Názov zariadenia",
+ "Last seen": "Naposledy aktívne",
+ "Failed to set display name": "Nepodarilo sa nastaviť zobrazované meno",
+ "Authentication": "Overenie",
+ "Failed to delete device": "Nepodarilo sa vymazať zariadenie",
+ "Delete": "Vymazať",
+ "Disable Notifications": "Zakázať oznámenia",
+ "Enable Notifications": "Povoliť oznámenia",
+ "Cannot add any more widgets": "Nie je možné pridať ďalšie widgety",
+ "The maximum permitted number of widgets have already been added to this room.": "Do tejto miestnosti už bol pridaný maximálny povolený počet widgetov.",
+ "Add a widget": "Pridať widget",
+ "Drop File Here": "Pretiahnite sem súbor",
+ "Drop file here to upload": "Pretiahnutím sem nahráte súbor",
+ " (unsupported)": " (nepodporované)",
+ "Join as voice or video.": "Pripojte sa ako audio alebo video.",
+ "Ongoing conference call%(supportedText)s.": "Prebiehajúci%(supportedText)s hovor.",
+ "%(senderName)s sent an image": "%(senderName)s poslal obrázok",
+ "%(senderName)s sent a video": "%(senderName)s poslal video",
+ "%(senderName)s uploaded a file": "%(senderName)s nahral súbor",
+ "Options": "Možnosti",
+ "Undecryptable": "Nedešifrovateľné",
+ "Encrypted by a verified device": "Zašifrované overeným zariadením",
+ "Encrypted by an unverified device": "Zašifrované neovereným zariadením",
+ "Unencrypted message": "Nešifrovaná správa",
+ "Please select the destination room for this message": "Prosím, vyberte cieľovú miestnosť pre túto správu",
+ "Blacklisted": "Na čiernej listine",
+ "Verified": "Overené",
+ "Unverified": "Neoverené",
+ "device id: ": "ID zariadenia: ",
+ "Disinvite": "Stiahnuť pozvanie",
+ "Kick": "Vykopnúť",
+ "Disinvite this user?": "Stiahnuť pozvanie tohoto používateľa?",
+ "Kick this user?": "Vykopnúť tohoto používateľa?",
+ "Failed to kick": "Nepodarilo sa vykopnúť",
+ "Unban": "Povoliť vstup",
+ "Ban": "Zakázať vstup",
+ "Unban this user?": "Povoliť vstúpiť tomuto používateľovi?",
+ "Ban this user?": "Zakázať vstúpiť tomuto používateľovi?",
+ "Failed to ban user": "Nepodarilo sa zakázať vstup používateľa",
+ "Failed to mute user": "Nepodarilo sa umlčať používateľa",
+ "Failed to toggle moderator status": "Nepodarilo sa prepnúť stav moderátor",
+ "Failed to change power level": "Nepodarilo sa zmeniť úroveň moci",
+ "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "Túto zmenu nebudete môcť vrátiť späť pretože tomuto používateľovi udeľujete rovnakú úroveň moci, akú máte vy.",
+ "Are you sure?": "Ste si istí?",
+ "No devices with registered encryption keys": "Žiadne zariadenia so zaregistrovanými šifrovacími kľúčmi",
+ "Devices": "Zariadenia",
+ "Unignore": "Prestať ignorovať",
+ "Ignore": "Ignorovať",
+ "Jump to read receipt": "Preskočiť na potvrdenie o prečítaní",
+ "Mention": "Zmieniť sa",
+ "Invite": "Pozvať",
+ "User Options": "Možnosti používateľa",
+ "Direct chats": "Priame konverzácie",
+ "Unmute": "Zrušiť umlčanie",
+ "Mute": "Umlčať",
+ "Revoke Moderator": "Odobrať stav moderátor",
+ "Make Moderator": "Udeliť stav moderátor",
+ "Admin Tools": "Nástroje správcu",
+ "Level:": "Úroveň:",
+ "and %(count)s others...|other": "a ďalších %(count)s...",
+ "and %(count)s others...|one": "a jeden ďalší...",
+ "Invited": "Pozvaní",
+ "Filter room members": "Filtrovať členov v miestnosti",
+ "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (moc %(powerLevelNumber)s)",
+ "Attachment": "Príloha",
+ "Upload Files": "Nahrať súbory",
+ "Are you sure you want to upload the following files?": "Ste si istí, že chcete nahrať nasledujúce súbory?",
+ "Encrypted room": "Šifrovaná miestnosť",
+ "Unencrypted room": "Nešifrovaná miestnosť",
+ "Hangup": "Zavesiť",
+ "Voice call": "Audio hovor",
+ "Video call": "Video hovor",
+ "Hide Apps": "Skriť aplikácie",
+ "Show Apps": "Zobraziť aplikácie",
+ "Upload file": "Nahrať súbor",
+ "Show Text Formatting Toolbar": "Zobraziť lištu formátovania textu",
+ "Send an encrypted message": "Odoslať šifrovanú správu",
+ "Send a message (unencrypted)": "Odoslať správu (nešifrovanú)",
+ "You do not have permission to post to this room": "Nemáte udelené právo posielať do tejto miestnosti",
+ "Turn Markdown on": "Povoliť Markdown",
+ "Turn Markdown off": "Zakázať Markdown",
+ "Hide Text Formatting Toolbar": "Skriť lištu formátovania textu",
+ "Server error": "Chyba servera",
+ "Server unavailable, overloaded, or something else went wrong.": "Server je nedostupný, preťažený, alebo sa pokazilo niečo iné.",
+ "Command error": "Chyba príkazu",
+ "bold": "tučné",
+ "italic": "kurzíva",
+ "strike": "preškrtnutie",
+ "underline": "podčiarknutie",
+ "code": "kód",
+ "quote": "citácia",
+ "bullet": "odrážky",
+ "numbullet": "číslované odrážky",
+ "Markdown is disabled": "Markdown je zakázaný",
+ "Markdown is enabled": "Markdown je povolený",
+ "Unpin Message": "Zrušiť pripnutie správy",
+ "Jump to message": "Preskočiť na správu",
+ "No pinned messages.": "Žiadne pripnuté správy.",
+ "Loading...": "Načítanie...",
+ "Pinned Messages": "Pripnuté správy",
+ "for %(amount)ss": "na %(amount)ss",
+ "for %(amount)sm": "na %(amount)sm",
+ "for %(amount)sh": "na %(amount)sh",
+ "for %(amount)sd": "na %(amount)sd",
+ "Online": "Prítomný",
+ "Idle": "Nečinný",
+ "Offline": "Nedostupný",
+ "Unknown": "Neznámy",
+ "Seen by %(userName)s at %(dateTime)s": "%(userName)s videl %(dateTime)s",
+ "Unnamed room": "Nepomenovaná miestnosť",
+ "World readable": "Viditeľné pre všetkých",
+ "Guests can join": "Aj hostia môžu vstúpiť",
+ "No rooms to show": "Žiadne miestnosti na zobrazenie",
+ "Failed to set avatar.": "Nepodarilo sa nastaviť avatara.",
+ "Save": "Uložiť",
+ "(~%(count)s results)|other": "(~%(count)s výsledkov)",
+ "(~%(count)s results)|one": "(~%(count)s výsledok)",
+ "Join Room": "Vstúpiť do miestnosti",
+ "Upload avatar": "Nahrať avatara",
+ "Remove avatar": "Odstrániť avatara",
+ "Settings": "Nastavenia",
+ "Forget room": "Zabudnúť miestnosť",
+ "Search": "Hľadať",
+ "Show panel": "Zobraziť panel",
+ "Drop here to favourite": "Pretiahnutím sem označíte ako obľúbené",
+ "Drop here to tag direct chat": "Pretiahnutím sem označíte ako priamu konverzáciu",
+ "Drop here to restore": "Pretiahnutím sem obnovíte z pozadia",
+ "Drop here to demote": "Pretiahnutím sem presuniete do pozadia",
+ "Drop here to tag %(section)s": "Pretiahnutím sem pridáte značku %(section)s",
+ "Press to start a chat with someone": "Stlačením tlačidla otvoríte diskusiu s kýmkoľvek",
+ "You're not in any rooms yet! Press to make a room or to browse the directory": "Zatiaľ ste nevstúpili do žiadnej miestnosti! Stlačením tlačidla môžete vytvoriť novú miestnosť alebo si po stlačení tlačidla môžete prezrieť adresár miestností",
+ "Community Invites": "Pozvánky do komunity",
+ "Invites": "Pozvánky",
+ "Favourites": "Obľúbené",
+ "People": "Ľudia",
+ "Rooms": "Miestnosti",
+ "Low priority": "Nízka priorita",
+ "Historical": "Historické",
+ "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Nie je možné si byť istý že toto pozvanie bola odoslaná na emailovú adresu priradenú k vašemu účtu.",
+ "This invitation was sent to an email address which is not associated with this account:": "Toto pozvanie bolo odoslané na emailovú adresu, ktorá nie je priradená k tomuto účtu:",
+ "You may wish to login with a different account, or add this email to this account.": "Môžete sa prihlásiť k inému účtu, alebo pridať emailovú adresu do práve prihláseného účtu.",
+ "You have been invited to join this room by %(inviterName)s": "Používateľ %(inviterName)s vás pozval vstúpiť do tejto miestnosti",
+ "Would you like to accept or decline this invitation?": "Chcete prijať alebo odmietnuť toto pozvanie?",
+ "Reason: %(reasonText)s": "Dôvod: %(reasonText)s",
+ "Rejoin": "Vstúpiť znovu",
+ "You have been kicked from %(roomName)s by %(userName)s.": "Používateľ %(userName)s vás vykopol z miestnosti %(roomName)s.",
+ "You have been kicked from this room by %(userName)s.": "Používateľ %(userName)s vás vykopol z tejto miestnosti.",
+ "You have been banned from %(roomName)s by %(userName)s.": "Používateľ %(userName)s vám zakázal vstúpiť do miestnosti %(roomName)s.",
+ "You have been banned from this room by %(userName)s.": "Používateľ %(userName)s vám zakázal vstúpiť do tejto miestnosti.",
+ "This room": "Táto miestnosť",
+ "%(roomName)s does not exist.": "%(roomName)s neexistuje.",
+ "%(roomName)s is not accessible at this time.": "%(roomName)s nie je momentálne prístupná.",
+ "You are trying to access %(roomName)s.": "Pristupujete k miestnosti %(roomName)s.",
+ "You are trying to access a room.": "Pristupujete k miestnosti.",
+ "Click here to join the discussion!": "Kliknutím sem vstúpite do diskusie!",
+ "This is a preview of this room. Room interactions have been disabled": "Toto je náhľad na miestnosť. Všetky akcie pre túto miestnosť sú zakázané",
+ "To change the room's avatar, you must be a": "Aby ste mohli meniť avatara miestnosti, musíte byť",
+ "To change the room's name, you must be a": "Aby ste mohli meniť názov miestnosti, musíte byť",
+ "To change the room's main address, you must be a": "Aby ste mohli meniť hlavnú adresu miestnosti, musíte byť",
+ "To change the room's history visibility, you must be a": "Aby ste mohli meniť viditeľnosť histórie miestnosti, musíte byť",
+ "To change the permissions in the room, you must be a": "Aby ste mohli meniť oprávnenia v miestnosti, musíte byť",
+ "To change the topic, you must be a": "Aby ste mohli meniť tému, musíte byť",
+ "To modify widgets in the room, you must be a": "Aby ste v miestnosti mohli meniť widgety, musíte byť",
+ "Failed to unban": "Nepodarilo sa povoliť vstup",
+ "Banned by %(displayName)s": "Vstup zakázal %(displayName)s",
+ "Privacy warning": "Upozornenie súkromia",
+ "Changes to who can read history will only apply to future messages in this room": "Zmeny určujúce kto môže čítať históriu sa uplatnia len na budúce správy v tejto miestnosti",
+ "The visibility of existing history will be unchanged": "Viditeľnosť existujúcej histórie ostane bez zmeny",
+ "unknown error code": "neznámy kód chyby",
+ "Failed to forget room %(errCode)s": "Nepodarilo sa zabudnúť miestnosť %(errCode)s",
+ "End-to-end encryption is in beta and may not be reliable": "E2E šifrovanie je v štádiu beta a nemusí byť úplne spoľahlivé",
+ "You should not yet trust it to secure data": "Nemali by ste zatiaľ spoliehať, že vám toto šifrovanie dokáže zabezpečiť vaše údaje",
+ "Devices will not yet be able to decrypt history from before they joined the room": "Zariadenia zatiaľ nedokážu dešifrovať správy poslané skôr, než ste na nich vstúpili do miestnosti",
+ "Once encryption is enabled for a room it cannot be turned off again (for now)": "Ak v miestnosti povolíte šifrovanie, šifrovanie nie je viac možné zakázať (aspoň zatiaľ nie)",
+ "Encrypted messages will not be visible on clients that do not yet implement encryption": "Šifrované správy nebudú vôbec zobrazené v klientoch, ktorí zatiaľ nepodporujú šifrovanie",
+ "Never send encrypted messages to unverified devices in this room from this device": "Z tohoto zariadenia nikdy v tejto miestnosti neposielať šifrované správy na neoverené zariadenia",
+ "Enable encryption": "Povoliť šifrovanie",
+ "(warning: cannot be disabled again!)": "(Pozor: Nie je viac možné zakázať!)",
+ "Encryption is enabled in this room": "V tejto miestnosti je povolené šifrovanie",
+ "Encryption is not enabled in this room": "V tejto miestnosti nie je povolené šifrovanie",
+ "Privileged Users": "Poverení používatelia",
+ "%(user)s is a": "%(user)s je",
+ "No users have specific privileges in this room": "Žiadny používatelia nemajú v tejto miestnosti pridelené konkrétne poverenia",
+ "Banned users": "Používatelia, ktorým bol zakázaný vstup",
+ "This room is not accessible by remote Matrix servers": "Táto miestnosť nie je prístupná cez vzdialené Matrix servery",
+ "Leave room": "Opustiť miestnosť",
+ "Favourite": "Obľúbená",
+ "Tagged as: ": "Označená ako: ",
+ "To link to a room it must have an address.": "Ak chcete vytvoriť odkaz do miestnosti, musíte najprv nastaviť jej adresu.",
+ "Guests cannot join this room even if explicitly invited.": "Hostia nemôžu vstúpiť do tejto miestnosti ani ak ich priamo pozvete.",
+ "Click here to fix": "Kliknutím sem to opravíte",
+ "Who can access this room?": "Kto môže vstúpiť do tejto miestnosti?",
+ "Only people who have been invited": "Len pozvaní ľudia",
+ "Anyone who knows the room's link, apart from guests": "Ktokoľvek, kto pozná odkaz do miestnosti (okrem hostí)",
+ "Anyone who knows the room's link, including guests": "Ktokoľvek, kto pozná odkaz do miestnosti (vrátane hostí)",
+ "Publish this room to the public in %(domain)s's room directory?": "Uverejniť túto miestnosť v adresáry miestností na servery %(domain)s?",
+ "Who can read history?": "Kto môže čítať históriu?",
+ "Anyone": "Ktokoľvek",
+ "Members only (since the point in time of selecting this option)": "Len členovia (odkedy je aktívna táto voľba)",
+ "Members only (since they were invited)": "Len členovia (odkedy boli pozvaní)",
+ "Members only (since they joined)": "Len členovia (odkedy vstúpili)",
+ "Room Colour": "Farba miestnosti",
+ "Permissions": "Oprávnenia",
+ "The default role for new room members is": "Predvolený status pre nových členov je",
+ "To send messages, you must be a": "Aby ste mohli posielať správy, musíte byť",
+ "To invite users into the room, you must be a": "Aby ste mohli pozývať používateľov do miestnosti, musíte byť",
+ "To configure the room, you must be a": "Aby ste mohli nastavovať miestnosť, musíte byť",
+ "To kick users, you must be a": "Aby ste mohli vykopávať používateľov, musíte byť",
+ "To ban users, you must be a": "Aby ste používateľom mohli zakazovať vstup, musíte byť",
+ "To remove other users' messages, you must be a": "Aby ste mohli odstraňovať správy, ktoré poslali iní používatelia, musíte byť",
+ "To send events of type , you must be a": "Aby ste mohli posielať udalosti typu , musíte byť",
+ "Advanced": "Pokročilé",
+ "This room's internal ID is": "Interné ID tejto miestnosti je",
+ "Add a topic": "Pridať tému",
+ "Cancel": "Zrušiť",
+ "Scroll to unread messages": "Posunúť na neprečítané správy",
+ "Jump to first unread message.": "Preskočiť na prvú neprečítanú správu.",
+ "Close": "Zatvoriť",
+ "Invalid alias format": "Nesprávny formát aliasu",
+ "'%(alias)s' is not a valid format for an alias": "'%(alias)s' nie je platný formát pre alias",
+ "Invalid address format": "Nesprávny formát adresy",
+ "'%(alias)s' is not a valid format for an address": "'%(alias)s' nie je platný formát adresy",
+ "not specified": "nezadané",
+ "not set": "nenastavené",
+ "Remote addresses for this room:": "Vzdialené adresy do tejto miestnosti:",
+ "The main address for this room is": "Hlavná adresa tejto miestnosti je",
+ "Local addresses for this room:": "Lokálne adresy do tejto miestnosti:",
+ "This room has no local addresses": "Pre túto miestnosť nie sú žiadne lokálne adresy",
+ "New address (e.g. #foo:%(localDomain)s)": "Nová adresa (napr. #foo:%(localDomain)s)",
+ "Invalid community ID": "Nesprávne ID komunity",
+ "'%(groupId)s' is not a valid community ID": "'%(groupId)s' nie je platným ID komunity",
+ "Related Communities": "Súvisiace komunity",
+ "Related communities for this room:": "Komunity spojené s touto miestnosťou:",
+ "This room has no related communities": "Pre túto miestnosť nie sú žiadne súvisiace komunity",
+ "New community ID (e.g. +foo:%(localDomain)s)": "Nové ID komunity (napr. +foo:%(localDomain)s)",
+ "Disable URL previews by default for participants in this room": "Predvolene zakázať náhľady URL adries pre členov tejto miestnosti",
+ "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.": "Náhľady URL adries sú predvolene %(globalDisableUrlPreview)s pre členov tejto miestnosti.",
+ "disabled": "zakázané",
+ "enabled": "povolené",
+ "You have disabled URL previews by default.": "Predvolene máte zakázané náhľady URL adries.",
+ "You have enabled URL previews by default.": "Predvolene máte povolené náhľady URL adries.",
+ "URL Previews": "Náhľady URL adries",
+ "Enable URL previews for this room (affects only you)": "Povoliť náhľady URL adries pre túto miestnosť (ovplyvňuje len vás)",
+ "Disable URL previews for this room (affects only you)": "Zakázať náhľady URL adries pre túto miestnosť (ovplyvňuje len vás)",
+ "Error decrypting audio": "Chyba pri dešifrovaní zvuku",
+ "Error decrypting attachment": "Chyba pri dešifrovaní prílohy",
+ "Decrypt %(text)s": "Dešifrovať %(text)s",
+ "Download %(text)s": "Stiahnuť %(text)s",
+ "Invalid file%(extra)s": "Neplatný súbor%(extra)s",
+ "Error decrypting image": "Chyba pri dešifrovaní obrázka",
+ "Image '%(Body)s' cannot be displayed.": "Nie je možné zobraziť obrázok '%(Body)s'.",
+ "This image cannot be displayed.": "Tento obrázok nie je možné zobraziť.",
+ "Error decrypting video": "Chyba pri dešifrovaní videa",
+ "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s zmenil avatara pre %(roomName)s",
+ "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s z miestnosti odstránil avatara.",
+ "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s zmenil avatara miestnosti na ",
+ "Copied!": "Skopírované!",
+ "Failed to copy": "Nepodarilo sa skopírovať",
+ "Add an Integration": "Pridať integráciu",
+ "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?": "Budete presmerovaní na stránku tretej strany, aby ste mohli overiť svoj účet na použitie s %(integrationsUrl)s. Chcete pokračovať?",
+ "Removed or unknown message type": "Odstránený alebo neznámy typ udalosti",
+ "Message removed by %(userId)s": "Správu odstránil %(userId)s",
+ "Message removed": "správa odstránená",
+ "Robot check is currently unavailable on desktop - please use a web browser": "Overenie, že nieste robot nie je možné cez aplikáciu na pracovnej ploche - prosím prejdite do prehliadača webu",
+ "This Home Server would like to make sure you are not a robot": "Tento domovský server by sa rád uistil, že nieste robot",
+ "Sign in with CAS": "Prihlásiť sa s použitím CAS",
+ "Custom Server Options": "Vlastné možnosti servera",
+ "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "Vlastné nastavenia servera môžete použiť na pripojenie k iným serverom Matrix a to zadaním URL adresy domovského servera.",
+ "This allows you to use this app with an existing Matrix account on a different home server.": "Umožní vám to použiť túto aplikáciu s už existujúcim Matrix účtom na akomkoľvek domovskom servery.",
+ "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "Môžete tiež zadať vlastnú adresu servera totožností, čo však za štandardných okolností znemožní interakcie medzi používateľmi založené emailovou adresou.",
+ "Dismiss": "Zamietnuť",
+ "To continue, please enter your password.": "Aby ste mohli pokračovať, prosím zadajte svoje heslo.",
+ "Password:": "Heslo:",
+ "An email has been sent to %(emailAddress)s": "Na adresu %(emailAddress)s bola odoslaná správa",
+ "Please check your email to continue registration.": "Prosím, skontrolujte si emaily, aby ste mohli pokračovať v registrácii.",
+ "Token incorrect": "Nesprávny token",
+ "A text message has been sent to %(msisdn)s": "Na číslo %(msisdn)s bola odoslaná textová správa",
+ "Please enter the code it contains:": "Prosím, zadajte kód z tejto správy:",
+ "Start authentication": "Spustiť overenie",
+ "powered by Matrix": "Poháňa Matrix",
+ "User name": "Meno používateľa",
+ "Mobile phone number": "Číslo mobilného telefónu",
+ "Forgot your password?": "Zabudli ste heslo?",
+ "%(serverName)s Matrix ID": "Matrix ID na servery %(serverName)s",
+ "Sign in with": "Na prihlásenie sa použije",
+ "Email address": "Emailová adresa",
+ "Sign in": "Prihlásiť sa",
+ "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Ak nezadáte vašu emailovú adresu, nebudete si môcť obnoviť heslo. Ste si istí?",
+ "Email address (optional)": "Emailová adresa (nepovinné)",
+ "You are registering with %(SelectedTeamName)s": "Registrujete sa s %(SelectedTeamName)s",
+ "Mobile phone number (optional)": "Číslo mobilného telefónu (nepovinné)",
+ "Register": "Zaregistrovať",
+ "Default server": "Predvolený server",
+ "Custom server": "Vlastný server",
+ "Home server URL": "Adresa domovského servera",
+ "Identity server URL": "Adresa servera totožností",
+ "What does this mean?": "Čo je toto?",
+ "Remove from community": "Odstrániť z komunity",
+ "Disinvite this user from community?": "Zrušiť pozvanie tohoto používateľa z komunity?",
+ "Remove this user from community?": "Odstrániť tohoto používateľa z komunity?",
+ "Failed to withdraw invitation": "Nepodarilo sa stiahnuť pozvanie",
+ "Failed to remove user from community": "Nepodarilo sa odstrániť používateľa z komunity",
+ "Filter community members": "Filtrovať členov komunity",
+ "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Ste si istí, že chcete odstrániť miestnosť '%(roomName)s' z komunity %(groupId)s?",
+ "Removing a room from the community will also remove it from the community page.": "Keď odstránite miestnosť z komunity, odstráni sa aj odkaz do miestnosti zo stránky komunity.",
+ "Remove": "Odstrániť",
+ "Failed to remove room from community": "Nepodarilo sa odstrániť miestnosť z komunity",
+ "Failed to remove '%(roomName)s' from %(groupId)s": "Nepodarilo sa odstrániť miestnosť '%(roomName)s' z komunity %(groupId)s",
+ "Something went wrong!": "Niečo sa pokazilo!",
+ "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "Nie je možné aktualizovať viditeľnosť miestnosti '%(roomName)s' v komunite %(groupId)s.",
+ "Visibility in Room List": "Viditeľnosť v zozname miestností",
+ "Visible to everyone": "Viditeľná pre všetkých",
+ "Only visible to community members": "Viditeľná len pre členov komunity",
+ "Filter community rooms": "Filtrovať miestnosti v komunite",
+ "Unknown Address": "Neznáma adresa",
+ "NOTE: Apps are not end-to-end encrypted": "POZOR: Aplikácie nie sú šifrované",
+ "Do you want to load widget from URL:": "Chcete načítať widget z URL adresy:",
+ "Allow": "Povoliť",
+ "Delete Widget": "Vymazať widget",
+ "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Týmto vymažete widget pre všetkých používateľov v tejto miestnosti. Ste si istí, že chcete vymazať tento widget?",
+ "Delete widget": "Vymazať widget",
+ "Revoke widget access": "Odmietnuť prístup k widgetu",
+ "Edit": "Upraviť",
+ "Create new room": "Vytvoriť novú miestnosť",
+ "Unblacklist": "Odstrániť z čiernej listiny",
+ "Blacklist": "Pridať na čiernu listinu",
+ "Unverify": "Zrušiť overenie",
+ "Verify...": "Overiť...",
+ "No results": "Žiadne výsledky",
+ "Home": "Domov",
+ "Integrations Error": "Chyba integrácií",
+ "Could not connect to the integration server": "Nie je možné sa pripojiť k integračnému serveru",
+ "Manage Integrations": "Spravovať integrácie",
+ "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
+ "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s%(count)s krát vstúpili",
+ "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)svstúpili",
+ "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s%(count)s krát vstúpil",
+ "%(oneUser)sjoined %(count)s times|one": "%(oneUser)svstúpil",
+ "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s%(count)s krát opustili",
+ "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sopustili",
+ "%(oneUser)sleft %(count)s times|other": "%(oneUser)s%(count)s krát opustil",
+ "%(oneUser)sleft %(count)s times|one": "%(oneUser)sopustil",
+ "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s%(count)s krát vstúpili a opustili",
+ "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)svstúpili a opustili",
+ "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s%(count)s krát vstúpil a opustil",
+ "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)svstúpil a opustil",
+ "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s%(count)s krát opustili a znovu vstúpili",
+ "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sopustili a znovu vstúpili",
+ "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s%(count)s krát opustil a znovu vstúpil",
+ "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sopustil a znovu vstúpil",
+ "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s%(count)s krát odmietli pozvanie",
+ "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)sodmietly pozvanie",
+ "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s%(count)s krát odmietol pozvanie",
+ "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)sodmietol pozvanie",
+ "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)smali %(count)s krát stiahnuté pozvanie",
+ "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)smali stiahnuté pozvanie",
+ "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)smal %(count)s krát stiahnuté pozvanie",
+ "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)smal stiahnuté pozvanie",
+ "were invited %(count)s times|other": "boli %(count)s krát pozvaní",
+ "were invited %(count)s times|one": "boli pozvaní",
+ "was invited %(count)s times|other": "bol %(count)s krát pozvaný",
+ "was invited %(count)s times|one": "bol pozvaný",
+ "were banned %(count)s times|other": "mali %(count)s krát zakázaný vstup",
+ "were banned %(count)s times|one": "mali zakázaný vstup",
+ "was banned %(count)s times|other": "mal %(count)s krát zakázaný vstup",
+ "was banned %(count)s times|one": "mal zakázaný vstup",
+ "were unbanned %(count)s times|other": "mali %(count)s krát povolený vstup",
+ "were unbanned %(count)s times|one": "mali povolený vstup",
+ "was unbanned %(count)s times|other": "mal %(count)s krát povolený vstup",
+ "was unbanned %(count)s times|one": "mal povolený vstup",
+ "were kicked %(count)s times|other": "boli %(count)s krát vykopnutí",
+ "were kicked %(count)s times|one": "boli vykopnutí",
+ "was kicked %(count)s times|other": "bol %(count)s krát vykopnutý",
+ "was kicked %(count)s times|one": "bol vykopnutý",
+ "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)ssi %(count)s krát zmenili meno",
+ "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)ssi zmenili meno",
+ "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)ssi %(count)s krát zmenil meno",
+ "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)ssi zmenil meno",
+ "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)ssi %(count)s krát zmenili avatara",
+ "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)ssi zmenili avatara",
+ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)ssi %(count)s krát zmenil avatara",
+ "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)ssi zmenil avatara",
+ "%(items)s and %(count)s others|other": "%(items)s a %(count)s ďalší",
+ "%(items)s and %(count)s others|one": "%(items)s a jeden ďalší",
+ "%(items)s and %(lastItem)s": "%(items)s a tiež %(lastItem)s",
+ "Custom level": "Vlastná úroveň",
+ "Room directory": "Adresár miestností",
+ "Start chat": "Začať konverzáciu",
+ "And %(count)s more...|other": "A %(count)s ďalších...",
+ "ex. @bob:example.com": "pr. @jan:priklad.sk",
+ "Add User": "Pridať používateľa",
+ "Matrix ID": "Matrix ID",
+ "Matrix Room ID": "ID Matrix miestnosti",
+ "email address": "emailová adresa",
+ "Try using one of the following valid address types: %(validTypesList)s.": "Skúste použiť niektorý z nasledujúcich správnych typov adresy: %(validTypesList)s.",
+ "You have entered an invalid address.": "Zadali ste neplatnú adresu.",
+ "Create a new chat or reuse an existing one": "Vytvorte novú konverzáciu alebo sa pripojte už k existujúcej",
+ "Start new chat": "Začať novú konverzáciu",
+ "You already have existing direct chats with this user:": "S týmto používateľom už máte spoločné priame konverzácie:",
+ "Start chatting": "Začať konverzovať",
+ "Click on the button below to start chatting!": "Konverzovať môžete začať kliknutím na tlačidlo nižšie!",
+ "Start Chatting": "Začať konverzovať",
+ "Confirm Removal": "Potvrdiť odstránenie",
+ "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.": "Ste si istí, že chcete odstrániť (vymazať) túto udalosť? Všimnite si: ak vymažete zmenu názvu miestnosti alebo zmenu témy, môžete tak vrátiť zodpovedajúcu zmenu.",
+ "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "ID komunity môže obsahovať len znaky a-z, 0-9, alebo '=_-./'",
+ "Something went wrong whilst creating your community": "Niečo sa pokazilo počas vytvárania požadovanej komunity",
+ "Create Community": "Vytvoriť komunitu",
+ "Community Name": "Názov komunity",
+ "Example": "Príklad",
+ "Community ID": "ID komunity",
+ "example": "príklad",
+ "Create": "Vytvoriť",
+ "Create Room": "Vytvoriť miestnosť",
+ "Room name (optional)": "Názov miestnosti (nepovinné)",
+ "Advanced options": "Pokročilé voľby",
+ "Block users on other matrix homeservers from joining this room": "Blokovať vstup do tejto miestnosti používateľom z ostatných domovských serverov Matrix",
+ "This setting cannot be changed later!": "Toto nastavenie už viac nie je možné meniť!",
+ "Unknown error": "Neznáma chyba",
+ "Incorrect password": "Nesprávne heslo",
+ "Deactivate Account": "Deaktivovať účet",
+ "This will make your account permanently unusable. You will not be able to re-register the same user ID.": "Toto spôsobí, že váš účet nebude viac použiteľný. Tak tiež si nebudete môcť znovu zaregistrovať rovnaké používateľské ID.",
+ "This action is irreversible.": "Túto akciu nie je možné vrátiť späť.",
+ "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "Ak chcete overiť, či toto zariadenie je skutočne dôverihodné, kontaktujte jeho vlastníka iným spôsobom (napr. osobne alebo cez telefón) a opýtajte sa ho, či kľúč, ktorý má pre toto zariadenie zobrazený v nastaveniach sa zhoduje s kľúčom zobrazeným nižšie:",
+ "Device name": "Názov zariadenia",
+ "Device key": "Kľúč zariadenia",
+ "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "Ak sa kľúče zhodujú, stlačte tlačidlo Overiť nižšie. Ak sa nezhodujú, niekto ďalší odpočúva toto zariadenie a v takomto prípade by ste asi mali namiesto toho stlačiť tlačidlo Pridať na čiernu listinu.",
+ "In future this verification process will be more sophisticated.": "V budúcnosti plánujeme tento proces overovania zariadení zjednodušiť.",
+ "Verify device": "Overiť zariadenie",
+ "I verify that the keys match": "Overil som, kľúče sa zhodujú",
+ "An error has occurred.": "Vyskytla sa chyba.",
+ "OK": "OK",
+ "You added a new device '%(displayName)s', which is requesting encryption keys.": "Pridali ste nové zariadenie nazvané '%(displayName)s', ktoré žiada o šifrovacie kľúče.",
+ "Your unverified device '%(displayName)s' is requesting encryption keys.": "Vaše neoverené zariadenie nazvané '%(displayName)s' žiada o šifrovacie kľúče.",
+ "Start verification": "Spustiť overenie",
+ "Share without verifying": "Zdieľať bez overenia",
+ "Ignore request": "Ignorovať žiadosť",
+ "Loading device info...": "Načítanie informácií o zariadení...",
+ "Encryption key request": "Žiadosť o šifrovacie kľúče",
+ "Otherwise, click here to send a bug report.": "inak kliknutím sem nahláste chybu.",
+ "Unable to restore session": "Nie je možné obnoviť reláciu",
+ "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "Pri pokuse o obnovenie vašej predchádzajúcej relácie sa vyskytla chyba. Ak budete pokračovať, musíte sa znovu prihlásiť, a história šifrovaných konverzácii nebude viac čitateľná.",
+ "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Ak ste sa v minulosti prihlásili s novšou verziou programu Riot, vaša relácia nemusí byť kompatibilná s touto verziou. Zatvorte prosím toto okno a vráťte sa cez najnovšiu verziu Riot.",
+ "Continue anyway": "Napriek tomu pokračovať",
+ "Invalid Email Address": "Nesprávna emailová adresa",
+ "This doesn't appear to be a valid email address": "Zdá sa, že toto nie je platná emailová adresa",
+ "Verification Pending": "Nedokončené overenie",
+ "Please check your email and click on the link it contains. Once this is done, click continue.": "Prosím, skontrolujte si email a kliknite na odkaz v správe, ktorú sme vám poslali. Keď budete mať toto za sebou, kliknite na tlačidlo Pokračovať.",
+ "Unable to add email address": "Nie je možné pridať emailovú adresu",
+ "Unable to verify email address.": "Nie je možné overiť emailovú adresu.",
+ "This will allow you to reset your password and receive notifications.": "Toto vám umožní obnoviť si heslo a prijímať oznámenia emailom.",
+ "Skip": "Preskočiť",
+ "User names may only contain letters, numbers, dots, hyphens and underscores.": "Používateľské meno môže obsahovať len písmená, číslice, bodky, pomlčky a podčiarkovníky.",
+ "Username not available": "Používateľské meno nie je k dispozícii",
+ "Username invalid: %(errMessage)s": "Neplatné používateľské meno: %(errMessage)s",
+ "An error occurred: %(error_string)s": "Vyskytla sa chyba: %(error_string)s",
+ "Username available": "Používateľské meno je k dispozícii",
+ "To get started, please pick a username!": "Začnite tým, že si zvolíte používateľské meno!",
+ "This will be your account name on the homeserver, or you can pick a different server.": "Toto bude názov vašeho účtu na domovskom servery , alebo si môžete zvoliť iný server.",
+ "If you already have a Matrix account you can log in instead.": "Ak už máte Matrix účet, môžete sa hneď Prihlásiť.",
+ "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Momentálne sa ku všetkym neovereným zariadeniam správate ako by boli na čiernej listine; aby ste na tieto zariadenia mohli posielať správy, mali by ste ich overiť.",
+ "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Odporúčame vám prejsť procesom overenia pre všetky tieto zariadenia aby ste si potvrdili, že skutočne patria ich pravým vlastníkom, ak si to však želáte, môžete tiež znovu poslať správu bez overovania.",
+ "Room contains unknown devices": "V miestnosti sú neznáme zariadenia",
+ "\"%(RoomName)s\" contains devices that you haven't seen before.": "V miestnosti \"%(RoomName)s\" sa našli zariadenia, s ktorými ste doposiaľ nikdy nekomunikovali.",
+ "Unknown devices": "Neznáme zariadenia",
+ "Send anyway": "Napriek tomu odoslať",
+ "Private Chat": "Súkromná konverzácia",
+ "Public Chat": "Verejná konverzácia",
+ "Custom": "Vlastné",
+ "Alias (optional)": "Alias (nepovinné)",
+ "Name": "Názov",
+ "Topic": "Téma",
+ "Make this room private": "Urobiť z tejto miestnosti súkromnú miestnosť",
+ "Share message history with new users": "Zdieľať históriu s novými používateľmi",
+ "Encrypt room": "Zašifrovať miestnosť",
+ "You must register to use this functionality": "Aby ste mohli použiť túto vlastnosť, musíte byť zaregistrovaný",
+ "You must join the room to see its files": "Aby ste si mohli zobraziť zoznam súborov, musíte vstúpiť do miestnosti",
+ "There are no visible files in this room": "V tejto miestnosti nie sú žiadne viditeľné súbory",
+ "
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": "
HTML kód hlavnej stránky komunity
\n
\n Dlhý popis môžete použiť na predstavenie komunity novým členom, alebo uvedenie \n dôležitých odkazov\n
\n
\n Môžete tiež používať HTML značku 'img'\n
\n",
+ "Add rooms to the community summary": "Pridať miestnosti do prehľadu komunity",
+ "Which rooms would you like to add to this summary?": "Ktoré miestnosti si želáte pridať do tohoto prehľadu?",
+ "Add to summary": "Pridať do prehľadu",
+ "Failed to add the following rooms to the summary of %(groupId)s:": "Do prehľadu komunity %(groupId)s sa nepodarilo pridať nasledujúce miestnosti:",
+ "Add a Room": "Pridať miestnosť",
+ "Failed to remove the room from the summary of %(groupId)s": "Z prehľadu komunity %(groupId)s sa nepodarilo odstrániť miestnosť",
+ "The room '%(roomName)s' could not be removed from the summary.": "Nie je možné odstrániť miestnosť '%(roomName)s' z prehľadu.",
+ "Add users to the community summary": "Pridať používateľov do prehľadu komunity",
+ "Who would you like to add to this summary?": "Koho si želáte pridať do tohoto prehľadu?",
+ "Failed to add the following users to the summary of %(groupId)s:": "Do prehľadu komunity %(groupId)s sa nepodarilo pridať nasledujúcich používateľov:",
+ "Add a User": "Pridať používateľa",
+ "Failed to remove a user from the summary of %(groupId)s": "Z prehľadu komunity %(groupId)s sa nepodarilo odstrániť používateľa",
+ "The user '%(displayName)s' could not be removed from the summary.": "Nie je možné odstrániť používateľa '%(displayName)s' z prehľadu.",
+ "Failed to upload image": "Nepodarilo sa nahrať obrázok",
+ "Failed to update community": "Nepodarilo sa aktualizovať komunitu",
+ "Unable to accept invite": "Nie je možné prijať pozvanie",
+ "Unable to reject invite": "Nie je možné odmietnuť pozvanie",
+ "Leave Community": "Opustiť komunitu",
+ "Leave %(groupName)s?": "Opustiť komunitu %(groupName)s?",
+ "Leave": "Opustiť",
+ "Unable to leave room": "Nie je možné opustiť miestnosť",
+ "Community Settings": "Nastavenia komunity",
+ "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Tieto miestnosti sú zobrazené všetkým členom na stránke komunity. Členovia komunity môžu vstúpiť do miestnosti kliknutím.",
+ "Add rooms to this community": "Pridať miestnosti do tejto komunity",
+ "Featured Rooms:": "Hlavné miestnosti:",
+ "Featured Users:": "Významní používatelia:",
+ "%(inviter)s has invited you to join this community": "%(inviter)s vás pozval vstúpiť do tejto komunity",
+ "You are an administrator of this community": "Ste správcom tejto komunity",
+ "You are a member of this community": "Ste členom tejto komunity",
+ "Community Member Settings": "Nastavenia členstva v komunite",
+ "Publish this community on your profile": "Uverejniť túto komunitu vo vašom profile",
+ "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!": "Vaša komunita nemá vyplnený dlhý popis, ktorý tvorí stránku komunity viditeľnú jej členom. Kliknutím sem otvoríte nastavenia, kde ho môžete vyplniť!",
+ "Long Description (HTML)": "Dlhý popis (HTML)",
+ "Description": "Popis",
+ "Community %(groupId)s not found": "Komunita %(groupId)s nebola nájdená",
+ "This Home server does not support communities": "Tento domovský server nepodporuje komunity",
+ "Failed to load %(groupId)s": "Nepodarilo sa načítať komunitu %(groupId)s",
+ "Reject invitation": "Odmietnuť pozvanie",
+ "Are you sure you want to reject the invitation?": "Ste si istí, že chcete odmietnuť toto pozvanie?",
+ "Failed to reject invitation": "Nepodarilo sa odmietnuť pozvanie",
+ "Are you sure you want to leave the room '%(roomName)s'?": "Ste si istí, že chcete opustiť miestnosť '%(roomName)s'?",
+ "Failed to leave room": "Nepodarilo sa opustiť miestnosť",
+ "Signed Out": "Ste odhlásení",
+ "For security, this session has been signed out. Please sign in again.": "Kôli bezpečnosti ste boli odhlásení z tejto relácie. Prosím, prihláste sa znovu.",
+ "Logout": "Odhlásiť sa",
+ "Your Communities": "Vaše komunity",
+ "You're not currently a member of any communities.": "V súčasnosti nie ste členom žiadnej komunity.",
+ "Error whilst fetching joined communities": "Pri získavaní vašich komunít sa vyskytla chyba",
+ "Create a new community": "Vytvoriť novú komunitu",
+ "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Vytvorte si komunitu s cieľom zoskupiť miestnosti a používateľov! Zostavte si vlastnú domovskú stránku a vymedzte tak svoj priestor vo svete Matrix.",
+ "Join an existing community": "Vstúpiť do existujúcej komunity",
+ "To join an existing community you'll have to know its community identifier; this will look something like +example:matrix.org.": "Aby ste mohli vstúpiť do existujúcej komunity, musíte poznať jej identifikátor; Mal by vizerať nejako takto +priklad:matrix.org.",
+ "You have no visible notifications": "Nie sú k dispozícii žiadne oznámenia",
+ "Scroll to bottom of page": "Posunúť na spodok stránky",
+ "Connectivity to the server has been lost.": "Spojenie so serverom bolo prerušené.",
+ "Sent messages will be stored until your connection has returned.": "Odoslané správy ostanú uložené, kým sa spojenie nenadviaže znovu.",
+ "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Znovu odoslať všetky alebo zrušiť všetky teraz. Môžete tiež znovu poslať alebo zrušiť odosielanie jednotlivých správ zvlášť.",
+ "%(count)s new messages|other": "%(count)s nových správ",
+ "%(count)s new messages|one": "%(count)s nová správa",
+ "Active call": "Aktívny hovor",
+ "There's no one else here! Would you like to invite others or stop warning about the empty room?": "Okrem vás v tejto miestnosti nie je nik iný! Želáte si pozvať ďalších alebo prestať upozorňovať na prázdnu miestnosť?",
+ "You seem to be uploading files, are you sure you want to quit?": "Zdá sa, že práve nahrávate súbory, ste si istí, že chcete skončiť?",
+ "You seem to be in a call, are you sure you want to quit?": "Zdá sa, že máte prebiehajúci hovor, ste si istí, že chcete skončiť?",
+ "Some of your messages have not been sent.": "Niektoré vaše správy ešte neboli odoslané.",
+ "Message not sent due to unknown devices being present": "Neodoslaná správa kvôli nájdeným neznámym zariadeniam",
+ "Failed to upload file": "Nepodarilo sa nahrať súbor",
+ "Server may be unavailable, overloaded, or the file too big": "Server môže byť nedostupný, preťažený, alebo je súbor príliš veľký",
+ "Search failed": "Hľadanie zlyhalo",
+ "Server may be unavailable, overloaded, or search timed out :(": "Server môže byť nedostupný, preťažený, alebo vypršal časový limit hľadania :(",
+ "No more results": "Žiadne ďalšie výsledky",
+ "Unknown room %(roomId)s": "Neznáma miestnosť %(roomId)s",
+ "Room": "Miestnosť",
+ "Failed to save settings": "Nepodarilo sa uložiť nastavenia",
+ "Failed to reject invite": "Nepodarilo sa odmietnuť pozvanie",
+ "Fill screen": "Vyplniť obrazovku",
+ "Click to unmute video": "Kliknutím zrušíte stlmenie videa",
+ "Click to mute video": "Kliknutím stlmíte video",
+ "Click to unmute audio": "Kliknutím zrušíte stlmenie zvuku",
+ "Click to mute audio": "Kliknutím stlmíte zvuk",
+ "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Pri pokuse načítať konkrétny bod v histórii tejto miestnosti sa vyskytla chyba, nemáte oprávnenie na zobrazenie zodpovedajúcej správy.",
+ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Pri pokuse načítať konkrétny bod v histórii tejto miestnosti sa vyskytla chyba, Správu nie je možné nájsť.",
+ "Failed to load timeline position": "Nepodarilo sa načítať pozíciu na časovej osi",
+ "Uploading %(filename)s and %(count)s others|other": "Nahrávanie %(filename)s a %(count)s ďalších súborov",
+ "Uploading %(filename)s and %(count)s others|zero": "Nahrávanie %(filename)s",
+ "Uploading %(filename)s and %(count)s others|one": "Nahrávanie %(filename)s a %(count)s ďalší súbor",
+ "Autoplay GIFs and videos": "Automaticky prehrávať animované GIF obrázky a videá",
+ "Hide read receipts": "Skriť potvrdenia o prečítaní",
+ "Don't send typing notifications": "Neposielať oznámenia keď píšete",
+ "Always show message timestamps": "Vždy zobrazovať časovú značku správ",
+ "Show timestamps in 12 hour format (e.g. 2:30pm)": "Pri zobrazovaní časových značiek používať 12 hodinový formát (napr. 2:30pm)",
+ "Hide join/leave messages (invites/kicks/bans unaffected)": "Skriť správy o vstupe a opustení miestnosti (netýka sa pozvaní/vykopnutí/zákazov vstupu)",
+ "Hide avatar and display name changes": "Skriť zmeny zobrazovaného mena a avatara",
+ "Use compact timeline layout": "Použiť kompaktné rozloženie časovej osy",
+ "Hide removed messages": "Skriť odstránené správy",
+ "Enable automatic language detection for syntax highlighting": "Povoliť automatickú detegciu jazyka pre zvýrazňovanie syntaxe",
+ "Automatically replace plain text Emoji": "Automaticky nahrádzať textové Emoji",
+ "Disable Emoji suggestions while typing": "Zakázať návrhy Emoji počas písania",
+ "Hide avatars in user and room mentions": "Skriť avatarov pri zmienkach miestností a používateľov",
+ "Disable big emoji in chat": "Zakázať veľké Emoji v konverzácii",
+ "Mirror local video feed": "Zrkadliť lokálne video",
+ "Opt out of analytics": "Odhlásiť sa zo zberu analytických údajov",
+ "Disable Peer-to-Peer for 1:1 calls": "Zakázať P2P počas priamych volaní",
+ "Never send encrypted messages to unverified devices from this device": "Z tohoto zariadenia nikdy neposielať šifrované správy neovereným zariadeniam",
+ "Light theme": "Svetlá téma",
+ "Dark theme": "Tmavá téma",
+ "Can't load user settings": "Nie je možné načítať používateľské nastavenia",
+ "Server may be unavailable or overloaded": "Server môže byť nedostupný alebo preťažený",
+ "Sign out": "Odhlásiť sa",
+ "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "S cieľom posilniť bezpečnosť sa všetky E2E šifrovacie kľúče pri odhlásení odstránia z tohoto prehliadača. Ak chcete, aby ste mohli čítať históriu šifrovaných konverzácií aj po opätovnom prihlásení, prosím exportujte a bezpečne si uchovajte kľúče miestností.",
+ "Failed to change password. Is your password correct?": "Nepodarilo sa zmeniť heslo. Zadali ste správne heslo?",
+ "Success": "Úspech",
+ "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Úspešne ste si zmenili heslo. Na ostatných zariadeniach sa vám nebudú zobrazovať okamžité oznámenia, kým sa aj na nich opätovne neprihlásite",
+ "Remove Contact Information?": "Odstrániť kontaktné informácie?",
+ "Remove %(threePid)s?": "Odstrániť %(threePid)s?",
+ "Unable to remove contact information": "Nie je možné odstrániť kontaktné informácie",
+ "Refer a friend to Riot:": "Odporučte Riot známemu:",
+ "Interface Language": "Jazyk rozhrania",
+ "User Interface": "Používateľské rozhranie",
+ "Autocomplete Delay (ms):": "Oneskorenie automatického dokončovania (ms):",
+ "Disable inline URL previews by default": "Predvolene zakázať náhľady URL adries",
+ "": "",
+ "Import E2E room keys": "Importovať E2E kľúče miestností",
+ "Cryptography": "Kryptografia",
+ "Device ID:": "ID zariadenia:",
+ "Device key:": "Kľúč zariadenia:",
+ "Ignored Users": "Ignorovaní používatelia",
+ "Bug Report": "Hlásenie chyby",
+ "Found a bug?": "Našli ste chybu?",
+ "Report it": "Ohláste ju",
+ "Analytics": "Analytické údaje",
+ "Riot collects anonymous analytics to allow us to improve the application.": "Riot zbiera anonymné analytické údaje, čo nám umožňuje aplikáciu ďalej zlepšovať.",
+ "Labs": "Experimenty",
+ "These are experimental features that may break in unexpected ways": "Tieto funkcie sú experimentálne a môžu sa nečakane pokaziť",
+ "Use with caution": "Pri používaní buďte opatrní",
+ "Deactivate my account": "Deaktivovať môj účet",
+ "Clear Cache": "Vyprázdniť vyrovnávaciu pamäť",
+ "Clear Cache and Reload": "Vyprázdniť vyrovnávaciu pamäť a načítať znovu",
+ "Updates": "Aktualizácie",
+ "Check for update": "Skontrolovať dostupnosť aktualizácie",
+ "Reject all %(invitedRooms)s invites": "Odmietnuť všetky %(invitedRooms)s pozvania",
+ "Bulk Options": "Hromadné možnosti",
+ "Desktop specific": "Špecifické pre pracovnú plochu",
+ "Start automatically after system login": "Spustiť automaticky po prihlásení do systému",
+ "No media permissions": "Žiadne oprávnenia k médiám",
+ "You may need to manually permit Riot to access your microphone/webcam": "Mali by ste aplikácii Riot ručne udeliť právo pristupovať k mikrofónu a kamere",
+ "Missing Media Permissions, click here to request.": "Kliknutím sem vyžiadate chýbajúce oprávnenia na prístup k mediálnym zariadeniam.",
+ "No Microphones detected": "Neboli nájdené žiadne mikrofóny",
+ "No Webcams detected": "Neboli nájdené žiadne kamery",
+ "Default Device": "Predvolené zariadenie",
+ "Microphone": "Mikrofón",
+ "Camera": "Kamera",
+ "VoIP": "VoIP",
+ "Email": "Email",
+ "Add email address": "Pridať emailovú adresu",
+ "Notifications": "Oznámenia",
+ "Profile": "Profil",
+ "Display name": "Zobrazované meno",
+ "Account": "Účet",
+ "To return to your account in future you need to set a password": "Aby ste sa v budúcnosti mohli vrátiť k vašemu účtu mali by ste si teraz nastaviť heslo",
+ "Logged in as:": "Prihlásený ako:",
+ "Access Token:": "Prístupový token:",
+ "click to reveal": "Odkryjete kliknutím",
+ "Homeserver is": "Domovský server je",
+ "Identity Server is": "Server totožností je",
+ "matrix-react-sdk version:": "Verzia matrix-react-sdk:",
+ "riot-web version:": "Verzia riot-web:",
+ "olm version:": "Verzia olm:",
+ "Failed to send email": "Nepodarilo sa odoslať email",
+ "The email address linked to your account must be entered.": "Musíte zadať emailovú adresu prepojenú s vašim účtom.",
+ "A new password must be entered.": "Musíte zadať nové heslo.",
+ "New passwords must match each other.": "Obe nové heslá musia byť zhodné.",
+ "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Zmena hesla momentálne obnoví šifrovacie kľúče na všetkých vašich zariadeniach, čo spôsobí, že história vašich šifrovaných konverzácií sa stane nečitateľná, jedine že si pred zmenou hesla exportujete kľúče miestností do súboru a po zmene kľúče importujete naspäť. V budúcnosti bude táto funkcia vylepšená.",
+ "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Na adresu %(emailAddress)s bola odoslaná správa. Potom, čo prejdete na odkaz z tejto správy, kliknite nižšie.",
+ "I have verified my email address": "Overil som si emailovú adresu",
+ "Your password has been reset": "Vaše heslo bolo obnovené",
+ "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "Boli ste odhlásení na všetkych zariadeniach a nebudete viac dostávať okamžité oznámenia. Oznámenia znovu povolíte tak, že sa opätovne prihlásite na každom zariadení",
+ "Return to login screen": "Vrátiť sa na prihlasovaciu obrazovku",
+ "To reset your password, enter the email address linked to your account": "Ak chcete obnoviť vaše heslo, zadajte emailovú adresu prepojenú s vašim účtom",
+ "New password": "Nové heslo",
+ "Confirm your new password": "Potvrďte vaše nové heslo",
+ "Send Reset Email": "Poslať obnovovací email",
+ "Create an account": "Vytvoriť účet",
+ "This Home Server does not support login using email address.": "Tento domovský server nepodporuje prihlasovanie sa emailom.",
+ "Incorrect username and/or password.": "Nesprávne meno používateľa a / alebo heslo.",
+ "Guest access is disabled on this Home Server.": "Na tomto domovskom servery je zakázaný prístup pre hostí.",
+ "The phone number entered looks invalid": "Zdá sa, že zadané telefónne číslo je neplatné",
+ "Error: Problem communicating with the given homeserver.": "Chyba: Nie je možné komunikovať so zadaným domovským serverom.",
+ "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "K domovskému serveru nie je možné pripojiť sa použitím protokolu HTTP keďže v adresnom riadku prehliadača máte HTTPS adresu. Použite protokol HTTPS alebo povolte nezabezpečené skripty.",
+ "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.": "Nie je možné pripojiť sa k domovskému serveru - skontrolujte prosím funkčnosť vašeho pripojenia na internet, uistite sa že certifikát domovského servera je dôverihodný, a že žiaden doplnok nainštalovaný v prehliadači nemôže blokovať požiadavky.",
+ "Sorry, this homeserver is using a login which is not recognised ": "Prepáčte, tento domovský server používa neznámy spôsob prihlasovania ",
+ "Login as guest": "Prihlásiť sa ako hosť",
+ "Return to app": "Vrátiť sa do aplikácie",
+ "Failed to fetch avatar URL": "Nepodarilo sa získať URL adresu avatara",
+ "Set a display name:": "Nastaviť zobrazované meno:",
+ "Upload an avatar:": "Nahrať avatara:",
+ "This server does not support authentication with a phone number.": "Tento server nepodporuje overenie telefónnym číslom.",
+ "Missing password.": "Chýba heslo.",
+ "Passwords don't match.": "Heslá sa nezhodujú.",
+ "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Heslo je veľmi krátke (minimálne %(MIN_PASSWORD_LENGTH)s).",
+ "This doesn't look like a valid email address.": "Zdá sa, že toto nie je platná emailová adresa.",
+ "This doesn't look like a valid phone number.": "Zdá sa, že toto nie je platné telefónne číslo.",
+ "You need to enter a user name.": "Musíte zadať používateľské meno.",
+ "An unknown error occurred.": "Vyskytla sa neznáma chyba.",
+ "I already have an account": "Už mám účet",
+ "Displays action": "Zobrazí akciu",
+ "Bans user with given id": "Zakáže vstup používateľovi so zadaným ID",
+ "Unbans user with given id": "Povolí vstup používateľovi so zadaným ID",
+ "Define the power level of a user": "Určí úroveň sili používateľa",
+ "Deops user with given id": "Zruší stav moderátor používateľovi so zadaným ID",
+ "Invites user with given id to current room": "Pošle používateľovi so zadaným ID pozvanie do tejto miestnosti",
+ "Joins room with given alias": "Vstúpi do miestnosti so zadaným aliasom",
+ "Sets the room topic": "Nastaví tému miestnosti",
+ "Kicks user with given id": "Vykopne používateľa so zadaným ID",
+ "Changes your display nickname": "Zmení vaše zobrazované meno",
+ "Searches DuckDuckGo for results": "Vyhľadá výsledky na DuckDuckGo",
+ "Changes colour scheme of current room": "Zmení farebnú schému aktuálnej miestnosti",
+ "Verifies a user, device, and pubkey tuple": "Overí zadané údaje používateľa, zariadenie a verejný kľúč",
+ "Ignores a user, hiding their messages from you": "Ignoruje používateľa a skrije všetky jeho správy",
+ "Stops ignoring a user, showing their messages going forward": "Prestane ignorovať používateľa a začne zobrazovať jeho správy",
+ "Commands": "Príkazy",
+ "Results from DuckDuckGo": "Výsledky z DuckDuckGo",
+ "Emoji": "Emoji",
+ "Notify the whole room": "Oznamovať celú miestnosť",
+ "Room Notification": "Oznámenie miestnosti",
+ "Users": "Používatelia",
+ "unknown device": "neznáme zariadenie",
+ "NOT verified": "NE-overené",
+ "verified": "overené",
+ "Verification": "Overenie",
+ "Ed25519 fingerprint": "Odtlačok prsta Ed25519",
+ "User ID": "ID používateľa",
+ "Curve25519 identity key": "Kľúč totožnosti Curve25519",
+ "none": "žiadny",
+ "Claimed Ed25519 fingerprint key": "Údajne kľúč s odtlačkom prsta Ed25519",
+ "Algorithm": "Algoritmus",
+ "unencrypted": "nešifrované",
+ "Decryption error": "Chyba dešifrovania",
+ "Session ID": "ID relácie",
+ "End-to-end encryption information": "Informácie o šifrovaní E2E",
+ "Event information": "Informácie o udalosti",
+ "Sender device information": "Informácie o zariadení odosielateľa",
+ "Passphrases must match": "Heslá sa musia zhodovať",
+ "Passphrase must not be empty": "Heslo nesmie byť prázdne",
+ "Export room keys": "Exportovať kľúče miestností",
+ "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.": "Tento proces vás prevedie exportom kľúčov určených na dešifrovanie správ, ktoré ste dostali v šifrovaných miestnostiach do lokálneho súboru. Tieto kľúče zo súboru môžete neskôr importovať do iného Matrix klienta, aby ste v ňom mohli dešifrovať vaše šifrované správy.",
+ "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.": "Tento súbor umožní komukoľvek, k to má ku nemu prístup dešifrovať všetky vami viditeľné šifrované správy, mali by ste teda byť opatrní a tento súbor si bezpečne uchovať. Aby bolo toto pre vás jednoduchšie, nižšie zadajte heslo, ktorým budú údaje v súbore zašifrované. Importovať údaje zo súboru bude možné len po zadaní tohoto istého hesla.",
+ "Enter passphrase": "Zadajte heslo",
+ "Confirm passphrase": "Potvrďte heslo",
+ "Export": "Exportovať",
+ "Import room keys": "Importovať kľúče miestností",
+ "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.": "Tento proces vás prevedie importom šifrovacích kľúčov, ktoré ste si v minulosti exportovali v inom Matrix klientovi. Po úspešnom importe budete v tomto klientovi môcť dešifrovať všetky správy, ktoré ste mohli dešifrovať v spomínanom klientovi.",
+ "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Exportovaný súbor je chránený heslom. Súbor môžete importovať len ak zadáte zodpovedajúce heslo.",
+ "File to import": "Importovať zo súboru",
+ "Import": "Importovať",
+ "Show these rooms to non-members on the community page and room list?": "Zobrazovať tieto miestnosti na domovskej stránke komunity a v zozname miestností aj pre nečlenov?"
+}
diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index 1ef3ee1edd..078147640a 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -6,7 +6,7 @@
"Dismiss": "Відхилити",
"Drop here %(toAction)s": "Кидайте сюди %(toAction)s",
"Error": "Помилка",
- "Failed to forget room %(errCode)s": "Не вдалось забути кімнату %(errCode)s",
+ "Failed to forget room %(errCode)s": "Не вдалось видалити кімнату %(errCode)s",
"Favourite": "Вибране",
"Mute": "Стишити",
"Notifications": "Сповіщення",
@@ -96,5 +96,12 @@
"Email address": "Адреса е-почти",
"Email address (optional)": "Адреса е-почти (не обов'язково)",
"Email, name or matrix ID": "Е-почта, ім'я або matrix ID",
- "Failed to send email": "Помилка відправки е-почти"
+ "Failed to send email": "Помилка відправки е-почти",
+ "Edit": "Редактувати",
+ "Unpin Message": "Відкріпити повідомлення",
+ "Register": "Зарегіструватись",
+ "Rooms": "Кімнати",
+ "Add rooms to this community": "Добавити кімнати в це суспільство",
+ "This email address is already in use": "Ця адреса елект. почти вже використовується",
+ "This phone number is already in use": "Цей телефонний номер вже використовується"
}
diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index a10092e3d2..6e0cfd3892 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -239,7 +239,7 @@
"Device ID:": "裝置 ID:",
"device id: ": "裝置 ID: ",
"Reason": "原因",
- "Register": "注冊",
+ "Register": "註冊",
"Default server": "預設伺服器",
"Custom server": "自定的伺服器",
"Home server URL": "自家伺服器網址",
@@ -774,5 +774,6 @@
"Robot check is currently unavailable on desktop - please use a web browser": "機器人檢查目前在桌面端不可用 ── 請使用網路瀏覽器",
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s 小工具已被 %(senderName)s 修改",
"Copied!": "已複製!",
- "Failed to copy": "複製失敗"
+ "Failed to copy": "複製失敗",
+ "Add rooms to this community": "新增聊天室到此社群"
}
diff --git a/src/languageHandler.js b/src/languageHandler.js
index da62bfee56..59d71505a4 100644
--- a/src/languageHandler.js
+++ b/src/languageHandler.js
@@ -19,8 +19,7 @@ import request from 'browser-request';
import counterpart from 'counterpart';
import Promise from 'bluebird';
import React from 'react';
-
-import UserSettingsStore from './UserSettingsStore';
+import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
const i18nFolder = 'i18n/';
@@ -35,12 +34,9 @@ export function _td(s) {
return s;
}
-// The translation function. This is just a simple wrapper to counterpart,
-// but exists mostly because we must use the same counterpart instance
-// between modules (ie. here (react-sdk) and the app (riot-web), and if we
-// just import counterpart and use it directly, we end up using a different
-// instance.
-export function _t(...args) {
+// Wrapper for counterpart's translation function so that it handles nulls and undefineds properly
+// Takes the same arguments as counterpart.translate()
+function safeCounterpartTranslate(...args) {
// Horrible hack to avoid https://github.com/vector-im/riot-web/issues/4191
// The interpolation library that counterpart uses does not support undefined/null
// values and instead will throw an error. This is a problem since everywhere else
@@ -51,11 +47,11 @@ export function _t(...args) {
if (args[1] && typeof args[1] === 'object') {
Object.keys(args[1]).forEach((k) => {
if (args[1][k] === undefined) {
- console.warn("_t called with undefined interpolation name: " + k);
+ console.warn("safeCounterpartTranslate called with undefined interpolation name: " + k);
args[1][k] = 'undefined';
}
if (args[1][k] === null) {
- console.warn("_t called with null interpolation name: " + k);
+ console.warn("safeCounterpartTranslate called with null interpolation name: " + k);
args[1][k] = 'null';
}
});
@@ -64,75 +60,153 @@ export function _t(...args) {
}
/*
- * Translates stringified JSX into translated JSX. E.g
- * _tJsx(
- * "click here now",
- * /(.*?)<\/a>/,
- * (sub) => { return { sub }; }
- * );
+ * Translates text and optionally also replaces XML-ish elements in the text with e.g. React components
+ * @param {string} text The untranslated text, e.g "click here now to %(foo)s".
+ * @param {object} variables Variable substitutions, e.g { foo: 'bar' }
+ * @param {object} tags Tag substitutions e.g. { 'a': (sub) => {sub} }
*
- * @param {string} jsxText The untranslated stringified JSX e.g "click here now".
- * This will be translated by passing the string through to _t(...)
+ * In both variables and tags, the values to substitute with can be either simple strings, React components,
+ * or functions that return the value to use in the substitution (e.g. return a React component). In case of
+ * a tag replacement, the function receives as the argument the text inside the element corresponding to the tag.
*
- * @param {RegExp|RegExp[]} patterns A regexp to match against the translated text.
- * The captured groups from the regexp will be fed to 'sub'.
- * Only the captured groups will be included in the output, the match itself is discarded.
- * If multiple RegExps are provided, the function at the same position will be called. The
- * match will always be done from left to right, so the 2nd RegExp will be matched against the
- * remaining text from the first RegExp.
+ * Use tag substitutions if you need to translate text between tags (e.g. "Click here!"), otherwise
+ * you will end up with literal "" in your output, rather than HTML. Note that you can also use variable
+ * substitution to insert React components, but you can't use it to translate text between tags.
*
- * @param {Function|Function[]} subs A function which will be called
- * with multiple args, each arg representing a captured group of the matching regexp.
- * This function must return a JSX node.
- *
- * @return a React component containing the generated text
+ * @return a React component if any non-strings were used in substitutions, otherwise a string
*/
-export function _tJsx(jsxText, patterns, subs) {
- // convert everything to arrays
- if (patterns instanceof RegExp) {
- patterns = [patterns];
- }
- if (subs instanceof Function) {
- subs = [subs];
- }
- // sanity checks
- if (subs.length !== patterns.length || subs.length < 1) {
- throw new Error(`_tJsx: programmer error. expected number of RegExps == number of Functions: ${subs.length} != ${patterns.length}`);
- }
- for (let i = 0; i < subs.length; i++) {
- if (!(patterns[i] instanceof RegExp)) {
- throw new Error(`_tJsx: programmer error. expected RegExp for text: ${jsxText}`);
- }
- if (!(subs[i] instanceof Function)) {
- throw new Error(`_tJsx: programmer error. expected Function for text: ${jsxText}`);
- }
- }
+export function _t(text, variables, tags) {
+ // Don't do subsitutions in counterpart. We handle it ourselves so we can replace with React components
+ // However, still pass the variables to counterpart so that it can choose the correct plural if count is given
+ // It is enough to pass the count variable, but in the future counterpart might make use of other information too
+ const args = Object.assign({ interpolate: false }, variables);
// The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution)
- const tJsxText = _t(jsxText, {interpolate: false});
- const output = [tJsxText];
+ const translated = safeCounterpartTranslate(text, args);
- for (let i = 0; i < patterns.length; i++) {
- // convert the last element in 'output' into 3 elements (pre-text, sub function, post-text).
- // Rinse and repeat for other patterns (using post-text).
- const inputText = output.pop();
- const match = inputText.match(patterns[i]);
- if (!match) {
- throw new Error(`_tJsx: translator error. expected translation to match regexp: ${patterns[i]}`);
+ return substitute(translated, variables, tags);
+}
+
+/*
+ * Similar to _t(), except only does substitutions, and no translation
+ * @param {string} text The text, e.g "click here now to %(foo)s".
+ * @param {object} variables Variable substitutions, e.g { foo: 'bar' }
+ * @param {object} tags Tag substitutions e.g. { 'a': (sub) => {sub} }
+ *
+ * The values to substitute with can be either simple strings, or functions that return the value to use in
+ * the substitution (e.g. return a React component). In case of a tag replacement, the function receives as
+ * the argument the text inside the element corresponding to the tag.
+ *
+ * @return a React component if any non-strings were used in substitutions, otherwise a string
+ */
+export function substitute(text, variables, tags) {
+ const regexpMapping = {};
+
+ if (variables !== undefined) {
+ for (const variable in variables) {
+ regexpMapping[`%\\(${variable}\\)s`] = variables[variable];
}
- const capturedGroups = match.slice(1);
-
- // Return the raw translation before the *match* followed by the return value of sub() followed
- // by the raw translation after the *match* (not captured group).
- output.push(inputText.substr(0, match.index));
- output.push(subs[i].apply(null, capturedGroups));
- output.push(inputText.substr(match.index + match[0].length));
}
- // this is a bit of a fudge to avoid the 'Each child in an array or iterator
- // should have a unique "key" prop' error: we explicitly pass the generated
- // nodes into React.createElement as children of a .
- return React.createElement('span', null, ...output);
+ if (tags !== undefined) {
+ for (const tag in tags) {
+ regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag];
+ }
+ }
+ return replaceByRegexes(text, regexpMapping);
+}
+
+/*
+ * Replace parts of a text using regular expressions
+ * @param {string} text The text on which to perform substitutions
+ * @param {object} mapping A mapping from regular expressions in string form to replacement string or a
+ * function which will receive as the argument the capture groups defined in the regexp. E.g.
+ * { 'Hello (.?) World': (sub) => sub.toUpperCase() }
+ *
+ * @return a React component if any non-strings were used in substitutions, otherwise a string
+ */
+export function replaceByRegexes(text, mapping) {
+ // We initially store our output as an array of strings and objects (e.g. React components).
+ // This will then be converted to a string or a at the end
+ const output = [text];
+
+ // If we insert any components we need to wrap the output in a span. React doesn't like just an array of components.
+ let shouldWrapInSpan = false;
+
+ for (const regexpString in mapping) {
+ // TODO: Cache regexps
+ const regexp = new RegExp(regexpString);
+
+ // Loop over what output we have so far and perform replacements
+ // We look for matches: if we find one, we get three parts: everything before the match, the replaced part,
+ // and everything after the match. Insert all three into the output. We need to do this because we can insert objects.
+ // Otherwise there would be no need for the splitting and we could do simple replcement.
+ let matchFoundSomewhere = false; // If we don't find a match anywhere we want to log it
+ for (const outputIndex in output) {
+ const inputText = output[outputIndex];
+ if (typeof inputText !== 'string') { // We might have inserted objects earlier, don't try to replace them
+ continue;
+ }
+
+ const match = inputText.match(regexp);
+ if (!match) {
+ continue;
+ }
+ matchFoundSomewhere = true;
+
+ const capturedGroups = match.slice(2);
+
+ // The textual part before the match
+ const head = inputText.substr(0, match.index);
+
+ // The textual part after the match
+ const tail = inputText.substr(match.index + match[0].length);
+
+ let replaced;
+ // If substitution is a function, call it
+ if (mapping[regexpString] instanceof Function) {
+ replaced = mapping[regexpString].apply(null, capturedGroups);
+ } else {
+ replaced = mapping[regexpString];
+ }
+
+ if (typeof replaced === 'object') {
+ shouldWrapInSpan = true;
+ }
+
+ output.splice(outputIndex, 1); // Remove old element
+
+ // Insert in reverse order as splice does insert-before and this way we get the final order correct
+ if (tail !== '') {
+ output.splice(outputIndex, 0, tail);
+ }
+
+ // Here we also need to check that it actually is a string before comparing against one
+ // The head and tail are always strings
+ if (typeof replaced !== 'string' || replaced !== '') {
+ output.splice(outputIndex, 0, replaced);
+ }
+
+ if (head !== '') { // Don't push empty nodes, they are of no use
+ output.splice(outputIndex, 0, head);
+ }
+ }
+ if (!matchFoundSomewhere) { // The current regexp did not match anything in the input
+ // Missing matches is entirely possible because you might choose to show some variables only in the case
+ // of e.g. plurals. It's still a bit suspicious, and could be due to an error, so log it.
+ // However, not showing count is so common that it's not worth logging. And other commonly unused variables
+ // here, if there are any.
+ if (regexpString !== '%\\(count\\)s') {
+ console.log(`Could not find ${regexp} in ${text}`);
+ }
+ }
+ }
+
+ if (shouldWrapInSpan) {
+ return React.createElement('span', null, ...output);
+ } else {
+ return output.join('');
+ }
}
// Allow overriding the text displayed when no translation exists
@@ -168,7 +242,7 @@ export function setLanguage(preferredLangs) {
}).then((langData) => {
counterpart.registerTranslations(langToUse, langData);
counterpart.setLocale(langToUse);
- UserSettingsStore.setLocalSetting('language', langToUse);
+ SettingsStore.setValue("language", null, SettingLevel.DEVICE, langToUse);
console.log("set language to " + langToUse);
// Set 'en' as fallback language:
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
new file mode 100644
index 0000000000..07de17ccfd
--- /dev/null
+++ b/src/settings/Settings.js
@@ -0,0 +1,258 @@
+/*
+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 {_td} from '../languageHandler';
+import {
+ AudioNotificationsEnabledController,
+ NotificationBodyEnabledController,
+ NotificationsEnabledController,
+} from "./controllers/NotificationControllers";
+
+
+// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
+const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config'];
+const LEVELS_ROOM_SETTINGS_WITH_ROOM = ['device', 'room-device', 'room-account', 'account', 'config', 'room'];
+const LEVELS_ACCOUNT_SETTINGS = ['device', 'account', 'config'];
+const LEVELS_FEATURE = ['device', 'config'];
+const LEVELS_DEVICE_ONLY_SETTINGS = ['device'];
+const LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG = ['device', 'config'];
+
+export const SETTINGS = {
+ // EXAMPLE SETTING:
+ // "my-setting": {
+ // // Must be set to true for features. Default is 'false'.
+ // isFeature: false,
+ //
+ // // Display names are strongly recommended for clarity.
+ // displayName: _td("Cool Name"),
+ //
+ // // Display name can also be an object for different levels.
+ // //displayName: {
+ // // "device": _td("Name for when the setting is used at 'device'"),
+ // // "room": _td("Name for when the setting is used at 'room'"),
+ // // "default": _td("The name for all other levels"),
+ // //}
+ //
+ // // The supported levels are required. Preferably, use the preset arrays
+ // // at the top of this file to define this rather than a custom array.
+ // supportedLevels: [
+ // // The order does not matter.
+ //
+ // "device", // Affects the current device only
+ // "room-device", // Affects the current room on the current device
+ // "room-account", // Affects the current room for the current account
+ // "account", // Affects the current account
+ // "room", // Affects the current room (controlled by room admins)
+ // "config", // Affects the current application
+ //
+ // // "default" is always supported and does not get listed here.
+ // ],
+ //
+ // // Required. Can be any data type. The value specified here should match
+ // // the data being stored (ie: if a boolean is used, the setting should
+ // // represent a boolean).
+ // default: {
+ // your: "value",
+ // },
+ //
+ // // Optional settings controller. See SettingsController for more information.
+ // controller: new MySettingController(),
+ //
+ // // Optional flag to make supportedLevels be respected as the order to handle
+ // // settings. The first element is treated as "most preferred". The "default"
+ // // level is always appended to the end.
+ // supportedLevelsAreOrdered: false,
+ // },
+ "feature_pinning": {
+ isFeature: true,
+ displayName: _td("Message Pinning"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
+ "feature_presence_management": {
+ isFeature: true,
+ displayName: _td("Presence Management"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
+ "feature_tag_panel": {
+ isFeature: true,
+ displayName: _td("Tag Panel"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
+ "MessageComposerInput.dontSuggestEmoji": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Disable Emoji suggestions while typing'),
+ default: false,
+ },
+ "useCompactLayout": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Use compact timeline layout'),
+ default: false,
+ },
+ "hideRedactions": {
+ supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
+ displayName: _td('Hide removed messages'),
+ default: false,
+ },
+ "hideJoinLeaves": {
+ supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
+ displayName: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
+ default: false,
+ },
+ "hideAvatarChanges": {
+ supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
+ displayName: _td('Hide avatar changes'),
+ default: false,
+ },
+ "hideDisplaynameChanges": {
+ supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
+ displayName: _td('Hide display name changes'),
+ default: false,
+ },
+ "hideReadReceipts": {
+ supportedLevels: LEVELS_ROOM_SETTINGS,
+ displayName: _td('Hide read receipts'),
+ default: false,
+ },
+ "showTwelveHourTimestamps": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
+ default: false,
+ },
+ "alwaysShowTimestamps": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Always show message timestamps'),
+ default: false,
+ },
+ "autoplayGifsAndVideos": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Autoplay GIFs and videos'),
+ default: false,
+ },
+ "enableSyntaxHighlightLanguageDetection": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Enable automatic language detection for syntax highlighting'),
+ default: false,
+ },
+ "Pill.shouldHidePillAvatar": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Hide avatars in user and room mentions'),
+ default: false,
+ },
+ "TextualBody.disableBigEmoji": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Disable big emoji in chat'),
+ default: false,
+ },
+ "MessageComposerInput.isRichTextEnabled": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ default: false,
+ },
+ "MessageComposer.showFormatting": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ default: false,
+ },
+ "dontSendTypingNotifications": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td("Don't send typing notifications"),
+ default: false,
+ },
+ "MessageComposerInput.autoReplaceEmoji": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Automatically replace plain text Emoji'),
+ default: false,
+ },
+ "VideoView.flipVideoHorizontally": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Mirror local video feed'),
+ default: false,
+ },
+ "theme": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ default: "light",
+ },
+ "webRtcForceTURN": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
+ displayName: _td('Disable Peer-to-Peer for 1:1 calls'),
+ default: false,
+ },
+ "webrtc_audioinput": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ default: null,
+ },
+ "webrtc_videoinput": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ default: null,
+ },
+ "language": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
+ default: "en",
+ },
+ "analyticsOptOut": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
+ displayName: _td('Opt out of analytics'),
+ default: false,
+ },
+ "autocompleteDelay": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
+ default: 200,
+ },
+ "blacklistUnverifiedDevices": {
+ // We specifically want to have room-device > device so that users may set a device default
+ // with a per-room override.
+ supportedLevels: ['room-device', 'device'],
+ supportedLevelsAreOrdered: true,
+ displayName: {
+ "default": _td('Never send encrypted messages to unverified devices from this device'),
+ "room-device": _td('Never send encrypted messages to unverified devices in this room from this device'),
+ },
+ default: false,
+ },
+ "urlPreviewsEnabled": {
+ supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
+ displayName: {
+ "default": _td('Enable inline URL previews by default'),
+ "room-account": _td("Enable URL previews for this room (only affects you)"),
+ "room": _td("Enable URL previews by default for participants in this room"),
+ },
+ default: true,
+ },
+ "roomColor": {
+ supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
+ displayName: _td("Room Colour"),
+ default: {
+ primary_color: null, // Hex string, eg: #000000
+ secondary_color: null, // Hex string, eg: #000000
+ },
+ },
+ "notificationsEnabled": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ default: false,
+ controller: new NotificationsEnabledController(),
+ },
+ "notificationBodyEnabled": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ default: true,
+ controller: new NotificationBodyEnabledController(),
+ },
+ "audioNotificationsEnabled": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ default: true,
+ controller: new AudioNotificationsEnabledController(),
+ },
+};
diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js
new file mode 100644
index 0000000000..d93a48005d
--- /dev/null
+++ b/src/settings/SettingsStore.js
@@ -0,0 +1,355 @@
+/*
+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 DeviceSettingsHandler from "./handlers/DeviceSettingsHandler";
+import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler";
+import DefaultSettingsHandler from "./handlers/DefaultSettingsHandler";
+import RoomAccountSettingsHandler from "./handlers/RoomAccountSettingsHandler";
+import AccountSettingsHandler from "./handlers/AccountSettingsHandler";
+import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
+import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
+import {_t} from '../languageHandler';
+import SdkConfig from "../SdkConfig";
+import {SETTINGS} from "./Settings";
+import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
+
+/**
+ * Represents the various setting levels supported by the SettingsStore.
+ */
+export const SettingLevel = {
+ // Note: This enum is not used in this class or in the Settings file
+ // This should always be used elsewhere in the project.
+ DEVICE: "device",
+ ROOM_DEVICE: "room-device",
+ ROOM_ACCOUNT: "room-account",
+ ACCOUNT: "account",
+ ROOM: "room",
+ CONFIG: "config",
+ DEFAULT: "default",
+};
+
+// Convert the settings to easier to manage objects for the handlers
+const defaultSettings = {};
+const featureNames = [];
+for (const key of Object.keys(SETTINGS)) {
+ defaultSettings[key] = SETTINGS[key].default;
+ if (SETTINGS[key].isFeature) featureNames.push(key);
+}
+
+const LEVEL_HANDLERS = {
+ "device": new DeviceSettingsHandler(featureNames),
+ "room-device": new RoomDeviceSettingsHandler(),
+ "room-account": new RoomAccountSettingsHandler(),
+ "account": new AccountSettingsHandler(),
+ "room": new RoomSettingsHandler(),
+ "config": new ConfigSettingsHandler(),
+ "default": new DefaultSettingsHandler(defaultSettings),
+};
+
+// Wrap all the handlers with local echo
+for (const key of Object.keys(LEVEL_HANDLERS)) {
+ LEVEL_HANDLERS[key] = new LocalEchoWrapper(LEVEL_HANDLERS[key]);
+}
+
+const LEVEL_ORDER = [
+ 'device', 'room-device', 'room-account', 'account', 'room', 'config', 'default',
+];
+
+/**
+ * Controls and manages application settings by providing varying levels at which the
+ * setting value may be specified. The levels are then used to determine what the setting
+ * value should be given a set of circumstances. The levels, in priority order, are:
+ * - "device" - Values are determined by the current device
+ * - "room-device" - Values are determined by the current device for a particular room
+ * - "room-account" - Values are determined by the current account for a particular room
+ * - "account" - Values are determined by the current account
+ * - "room" - Values are determined by a particular room (by the room admins)
+ * - "config" - Values are determined by the config.json
+ * - "default" - Values are determined by the hardcoded defaults
+ *
+ * Each level has a different method to storing the setting value. For implementation
+ * specific details, please see the handlers. The "config" and "default" levels are
+ * both always supported on all platforms. All other settings should be guarded by
+ * isLevelSupported() prior to attempting to set the value.
+ *
+ * Settings can also represent features. Features are significant portions of the
+ * application that warrant a dedicated setting to toggle them on or off. Features are
+ * special-cased to ensure that their values respect the configuration (for example, a
+ * feature may be reported as disabled even though a user has specifically requested it
+ * be enabled).
+ */
+export default class SettingsStore {
+ /**
+ * Gets the translated display name for a given setting
+ * @param {string} settingName The setting to look up.
+ * @param {"device"|"room-device"|"room-account"|"account"|"room"|"config"|"default"} atLevel
+ * The level to get the display name for; Defaults to 'default'.
+ * @return {String} The display name for the setting, or null if not found.
+ */
+ static getDisplayName(settingName, atLevel = "default") {
+ if (!SETTINGS[settingName] || !SETTINGS[settingName].displayName) return null;
+
+ let displayName = SETTINGS[settingName].displayName;
+ if (displayName instanceof Object) {
+ if (displayName[atLevel]) displayName = displayName[atLevel];
+ else displayName = displayName["default"];
+ }
+
+ return _t(displayName);
+ }
+
+ /**
+ * Returns a list of all available labs feature names
+ * @returns {string[]} The list of available feature names
+ */
+ static getLabsFeatures() {
+ const possibleFeatures = Object.keys(SETTINGS).filter((s) => SettingsStore.isFeature(s));
+
+ const enableLabs = SdkConfig.get()["enableLabs"];
+ if (enableLabs) return possibleFeatures;
+
+ return possibleFeatures.filter((s) => SettingsStore._getFeatureState(s) === "labs");
+ }
+
+ /**
+ * Determines if a setting is also a feature.
+ * @param {string} settingName The setting to look up.
+ * @return {boolean} True if the setting is a feature.
+ */
+ static isFeature(settingName) {
+ if (!SETTINGS[settingName]) return false;
+ return SETTINGS[settingName].isFeature;
+ }
+
+ /**
+ * Determines if a given feature is enabled. The feature given must be a known
+ * feature.
+ * @param {string} settingName The name of the setting that is a feature.
+ * @param {String} roomId The optional room ID to validate in, may be null.
+ * @return {boolean} True if the feature is enabled, false otherwise
+ */
+ static isFeatureEnabled(settingName, roomId = null) {
+ if (!SettingsStore.isFeature(settingName)) {
+ throw new Error("Setting " + settingName + " is not a feature");
+ }
+
+ return SettingsStore.getValue(settingName, roomId);
+ }
+
+ /**
+ * Sets a feature as enabled or disabled on the current device.
+ * @param {string} settingName The name of the setting.
+ * @param {boolean} value True to enable the feature, false otherwise.
+ * @returns {Promise} Resolves when the setting has been set.
+ */
+ static setFeatureEnabled(settingName, value) {
+ // Verify that the setting is actually a setting
+ if (!SETTINGS[settingName]) {
+ throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
+ }
+ if (!SettingsStore.isFeature(settingName)) {
+ throw new Error("Setting " + settingName + " is not a feature");
+ }
+
+ return SettingsStore.setValue(settingName, null, "device", value);
+ }
+
+ /**
+ * Gets the value of a setting. The room ID is optional if the setting is not to
+ * be applied to any particular room, otherwise it should be supplied.
+ * @param {string} settingName The name of the setting to read the value of.
+ * @param {String} roomId The room ID to read the setting value in, may be null.
+ * @param {boolean} excludeDefault True to disable using the default value.
+ * @return {*} The value, or null if not found
+ */
+ static getValue(settingName, roomId = null, excludeDefault = false) {
+ // Verify that the setting is actually a setting
+ if (!SETTINGS[settingName]) {
+ throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
+ }
+
+ const setting = SETTINGS[settingName];
+ const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
+
+ return SettingsStore.getValueAt(levelOrder[0], settingName, roomId, false, excludeDefault);
+ }
+
+ /**
+ * Gets a setting's value at a particular level, ignoring all levels that are more specific.
+ * @param {"device"|"room-device"|"room-account"|"account"|"room"|"config"|"default"} level The
+ * level to look at.
+ * @param {string} settingName The name of the setting to read.
+ * @param {String} roomId The room ID to read the setting value in, may be null.
+ * @param {boolean} explicit If true, this method will not consider other levels, just the one
+ * provided. Defaults to false.
+ * @param {boolean} excludeDefault True to disable using the default value.
+ * @return {*} The value, or null if not found.
+ */
+ static getValueAt(level, settingName, roomId = null, explicit = false, excludeDefault = false) {
+ // Verify that the setting is actually a setting
+ if (!SETTINGS[settingName]) {
+ throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
+ }
+
+ const setting = SETTINGS[settingName];
+ const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
+ if (!levelOrder.includes("default")) levelOrder.push("default"); // always include default
+
+ const minIndex = levelOrder.indexOf(level);
+ if (minIndex === -1) throw new Error("Level " + level + " is not prioritized");
+
+ if (SettingsStore.isFeature(settingName)) {
+ const configValue = SettingsStore._getFeatureState(settingName);
+ if (configValue === "enable") return true;
+ if (configValue === "disable") return false;
+ // else let it fall through the default process
+ }
+
+ const handlers = SettingsStore._getHandlers(settingName);
+
+ if (explicit) {
+ const handler = handlers[level];
+ if (!handler) return SettingsStore._tryControllerOverride(settingName, level, roomId, null);
+ const value = handler.getValue(settingName, roomId);
+ return SettingsStore._tryControllerOverride(settingName, level, roomId, value);
+ }
+
+ for (let i = minIndex; i < levelOrder.length; i++) {
+ const handler = handlers[levelOrder[i]];
+ if (!handler) continue;
+ if (excludeDefault && levelOrder[i] === "default") continue;
+
+ const value = handler.getValue(settingName, roomId);
+ if (value === null || value === undefined) continue;
+ return SettingsStore._tryControllerOverride(settingName, level, roomId, value);
+ }
+
+ return SettingsStore._tryControllerOverride(settingName, level, roomId, null);
+ }
+
+ static _tryControllerOverride(settingName, level, roomId, calculatedValue) {
+ const controller = SETTINGS[settingName].controller;
+ if (!controller) return calculatedValue;
+
+ const actualValue = controller.getValueOverride(level, roomId, calculatedValue);
+ if (actualValue !== undefined && actualValue !== null) return actualValue;
+ return calculatedValue;
+ }
+
+ /**
+ * Sets the value for a setting. The room ID is optional if the setting is not being
+ * set for a particular room, otherwise it should be supplied. The value may be null
+ * to indicate that the level should no longer have an override.
+ * @param {string} settingName The name of the setting to change.
+ * @param {String} roomId The room ID to change the value in, may be null.
+ * @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level
+ * to change the value at.
+ * @param {*} value The new value of the setting, may be null.
+ * @return {Promise} Resolves when the setting has been changed.
+ */
+ static setValue(settingName, roomId, level, value) {
+ // Verify that the setting is actually a setting
+ if (!SETTINGS[settingName]) {
+ throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
+ }
+
+ const handler = SettingsStore._getHandler(settingName, level);
+ if (!handler) {
+ throw new Error("Setting " + settingName + " does not have a handler for " + level);
+ }
+
+ if (!handler.canSetValue(settingName, roomId)) {
+ throw new Error("User cannot set " + settingName + " at " + level + " in " + roomId);
+ }
+
+ return handler.setValue(settingName, roomId, value).then(() => {
+ const controller = SETTINGS[settingName].controller;
+ if (!controller) return;
+ controller.onChange(level, roomId, value);
+ });
+ }
+
+ /**
+ * Determines if the current user is permitted to set the given setting at the given
+ * level for a particular room. The room ID is optional if the setting is not being
+ * set for a particular room, otherwise it should be supplied.
+ * @param {string} settingName The name of the setting to check.
+ * @param {String} roomId The room ID to check in, may be null.
+ * @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level to
+ * check at.
+ * @return {boolean} True if the user may set the setting, false otherwise.
+ */
+ static canSetValue(settingName, roomId, level) {
+ // Verify that the setting is actually a setting
+ if (!SETTINGS[settingName]) {
+ throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
+ }
+
+ const handler = SettingsStore._getHandler(settingName, level);
+ if (!handler) return false;
+ return handler.canSetValue(settingName, roomId);
+ }
+
+ /**
+ * Determines if the given level is supported on this device.
+ * @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level
+ * to check the feasibility of.
+ * @return {boolean} True if the level is supported, false otherwise.
+ */
+ static isLevelSupported(level) {
+ if (!LEVEL_HANDLERS[level]) return false;
+ return LEVEL_HANDLERS[level].isSupported();
+ }
+
+ static _getHandler(settingName, level) {
+ const handlers = SettingsStore._getHandlers(settingName);
+ if (!handlers[level]) return null;
+ return handlers[level];
+ }
+
+ static _getHandlers(settingName) {
+ if (!SETTINGS[settingName]) return {};
+
+ const handlers = {};
+ for (const level of SETTINGS[settingName].supportedLevels) {
+ if (!LEVEL_HANDLERS[level]) throw new Error("Unexpected level " + level);
+ handlers[level] = LEVEL_HANDLERS[level];
+ }
+
+ // Always support 'default'
+ if (!handlers['default']) handlers['default'] = LEVEL_HANDLERS['default'];
+
+ return handlers;
+ }
+
+ static _getFeatureState(settingName) {
+ const featuresConfig = SdkConfig.get()['features'];
+ const enableLabs = SdkConfig.get()['enableLabs']; // we'll honour the old flag
+
+ let featureState = enableLabs ? "labs" : "disable";
+ if (featuresConfig && featuresConfig[settingName] !== undefined) {
+ featureState = featuresConfig[settingName];
+ }
+
+ const allowedStates = ['enable', 'disable', 'labs'];
+ if (!allowedStates.includes(featureState)) {
+ console.warn("Feature state '" + featureState + "' is invalid for " + settingName);
+ featureState = "disable"; // to prevent accidental features.
+ }
+
+ return featureState;
+ }
+}
diff --git a/src/settings/controllers/NotificationControllers.js b/src/settings/controllers/NotificationControllers.js
new file mode 100644
index 0000000000..9dcf78e26b
--- /dev/null
+++ b/src/settings/controllers/NotificationControllers.js
@@ -0,0 +1,79 @@
+/*
+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 SettingController from "./SettingController";
+import MatrixClientPeg from '../../MatrixClientPeg';
+
+// XXX: This feels wrong.
+import PushProcessor from "matrix-js-sdk/lib/pushprocessor";
+
+function isMasterRuleEnabled() {
+ // Return the value of the master push rule as a default
+ const processor = new PushProcessor(MatrixClientPeg.get());
+ const masterRule = processor.getPushRuleById(".m.rule.master");
+
+ if (!masterRule) {
+ console.warn("No master push rule! Notifications are disabled for this user.");
+ return false;
+ }
+
+ // Why enabled == false means "enabled" is beyond me.
+ return !masterRule.enabled;
+}
+
+export class NotificationsEnabledController extends SettingController {
+ getValueOverride(level, roomId, calculatedValue) {
+ const Notifier = require('../../Notifier'); // avoids cyclical references
+ if (!Notifier.isPossible()) return false;
+
+ if (calculatedValue === null) {
+ return isMasterRuleEnabled();
+ }
+
+ return calculatedValue;
+ }
+
+ onChange(level, roomId, newValue) {
+ const Notifier = require('../../Notifier'); // avoids cyclical references
+
+ if (Notifier.supportsDesktopNotifications()) {
+ Notifier.setEnabled(newValue);
+ }
+ }
+}
+
+export class NotificationBodyEnabledController extends SettingController {
+ getValueOverride(level, roomId, calculatedValue) {
+ const Notifier = require('../../Notifier'); // avoids cyclical references
+ if (!Notifier.isPossible()) return false;
+
+ if (calculatedValue === null) {
+ return isMasterRuleEnabled();
+ }
+
+ return calculatedValue;
+ }
+}
+
+export class AudioNotificationsEnabledController extends SettingController {
+ getValueOverride(level, roomId, calculatedValue) {
+ const Notifier = require('../../Notifier'); // avoids cyclical references
+ if (!Notifier.isPossible()) return false;
+
+ // Note: Audio notifications are *not* enabled by default.
+ return calculatedValue;
+ }
+}
diff --git a/src/settings/controllers/SettingController.js b/src/settings/controllers/SettingController.js
new file mode 100644
index 0000000000..a91b616da9
--- /dev/null
+++ b/src/settings/controllers/SettingController.js
@@ -0,0 +1,49 @@
+/*
+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.
+*/
+
+/**
+ * Represents a controller for individual settings to alter the reading behaviour
+ * based upon environmental conditions, or to react to changes and therefore update
+ * the working environment.
+ *
+ * This is not intended to replace the functionality of a SettingsHandler, it is only
+ * intended to handle environmental factors for specific settings.
+ */
+export default class SettingController {
+
+ /**
+ * Gets the overridden value for the setting, if any. This must return null if the
+ * value is not to be overridden, otherwise it must return the new value.
+ * @param {string} level The level at which the value was requested at.
+ * @param {String} roomId The room ID, may be null.
+ * @param {*} calculatedValue The value that the handlers think the setting should be,
+ * may be null.
+ * @return {*} The value that should be used, or null if no override is applicable.
+ */
+ getValueOverride(level, roomId, calculatedValue) {
+ return null; // no override
+ }
+
+ /**
+ * Called when the setting value has been changed.
+ * @param {string} level The level at which the setting has been modified.
+ * @param {String} roomId The room ID, may be null.
+ * @param {*} newValue The new value for the setting, may be null.
+ */
+ onChange(level, roomId, newValue) {
+ // do nothing by default
+ }
+}
diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js
new file mode 100644
index 0000000000..e50358a728
--- /dev/null
+++ b/src/settings/handlers/AccountSettingsHandler.js
@@ -0,0 +1,76 @@
+/*
+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 SettingsHandler from "./SettingsHandler";
+import MatrixClientPeg from '../../MatrixClientPeg';
+
+/**
+ * Gets and sets settings at the "account" level for the current user.
+ * This handler does not make use of the roomId parameter.
+ */
+export default class AccountSettingHandler extends SettingsHandler {
+ getValue(settingName, roomId) {
+ // Special case URL previews
+ if (settingName === "urlPreviewsEnabled") {
+ const content = this._getSettings("org.matrix.preview_urls");
+
+ // Check to make sure that we actually got a boolean
+ if (typeof(content['disable']) !== "boolean") return null;
+ return !content['disable'];
+ }
+
+ let preferredValue = this._getSettings()[settingName];
+
+ if (preferredValue === null || preferredValue === undefined) {
+ // Honour the old setting on read only
+ if (settingName === "hideAvatarChanges" || settingName === "hideDisplaynameChanges") {
+ preferredValue = this._getSettings()["hideAvatarDisplaynameChanges"];
+ }
+ }
+
+ return preferredValue;
+ }
+
+ setValue(settingName, roomId, newValue) {
+ // Special case URL previews
+ if (settingName === "urlPreviewsEnabled") {
+ const content = this._getSettings("org.matrix.preview_urls");
+ content['disable'] = !newValue;
+ return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", content);
+ }
+
+ const content = this._getSettings();
+ content[settingName] = newValue;
+ return MatrixClientPeg.get().setAccountData("im.vector.web.settings", content);
+ }
+
+ canSetValue(settingName, roomId) {
+ return true; // It's their account, so they should be able to
+ }
+
+ isSupported() {
+ const cli = MatrixClientPeg.get();
+ return cli !== undefined && cli !== null;
+ }
+
+ _getSettings(eventType = "im.vector.web.settings") {
+ const cli = MatrixClientPeg.get();
+ if (!cli) return {};
+ const event = cli.getAccountData(eventType);
+ if (!event || !event.getContent()) return {};
+ return event.getContent();
+ }
+}
diff --git a/src/settings/handlers/ConfigSettingsHandler.js b/src/settings/handlers/ConfigSettingsHandler.js
new file mode 100644
index 0000000000..7a370249a7
--- /dev/null
+++ b/src/settings/handlers/ConfigSettingsHandler.js
@@ -0,0 +1,49 @@
+/*
+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 SettingsHandler from "./SettingsHandler";
+import SdkConfig from "../../SdkConfig";
+
+/**
+ * Gets and sets settings at the "config" level. This handler does not make use of the
+ * roomId parameter.
+ */
+export default class ConfigSettingsHandler extends SettingsHandler {
+ getValue(settingName, roomId) {
+ const config = SdkConfig.get() || {};
+
+ // Special case themes
+ if (settingName === "theme") {
+ return config["default_theme"];
+ }
+
+ const settingsConfig = config["settingDefaults"];
+ if (!settingsConfig || !settingsConfig[settingName]) return null;
+ return settingsConfig[settingName];
+ }
+
+ setValue(settingName, roomId, newValue) {
+ throw new Error("Cannot change settings at the config level");
+ }
+
+ canSetValue(settingName, roomId) {
+ return false;
+ }
+
+ isSupported() {
+ return true; // SdkConfig is always there
+ }
+}
diff --git a/src/settings/handlers/DefaultSettingsHandler.js b/src/settings/handlers/DefaultSettingsHandler.js
new file mode 100644
index 0000000000..cf2e660411
--- /dev/null
+++ b/src/settings/handlers/DefaultSettingsHandler.js
@@ -0,0 +1,48 @@
+/*
+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 SettingsHandler from "./SettingsHandler";
+
+/**
+ * Gets settings at the "default" level. This handler does not support setting values.
+ * This handler does not make use of the roomId parameter.
+ */
+export default class DefaultSettingsHandler extends SettingsHandler {
+ /**
+ * Creates a new default settings handler with the given defaults
+ * @param {object} defaults The default setting values, keyed by setting name.
+ */
+ constructor(defaults) {
+ super();
+ this._defaults = defaults;
+ }
+
+ getValue(settingName, roomId) {
+ return this._defaults[settingName];
+ }
+
+ setValue(settingName, roomId, newValue) {
+ throw new Error("Cannot set values on the default level handler");
+ }
+
+ canSetValue(settingName, roomId) {
+ return false;
+ }
+
+ isSupported() {
+ return true;
+ }
+}
diff --git a/src/settings/handlers/DeviceSettingsHandler.js b/src/settings/handlers/DeviceSettingsHandler.js
new file mode 100644
index 0000000000..22f6140a80
--- /dev/null
+++ b/src/settings/handlers/DeviceSettingsHandler.js
@@ -0,0 +1,114 @@
+/*
+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 Promise from 'bluebird';
+import SettingsHandler from "./SettingsHandler";
+import MatrixClientPeg from "../../MatrixClientPeg";
+
+/**
+ * Gets and sets settings at the "device" level for the current device.
+ * This handler does not make use of the roomId parameter. This handler
+ * will special-case features to support legacy settings.
+ */
+export default class DeviceSettingsHandler extends SettingsHandler {
+ /**
+ * Creates a new device settings handler
+ * @param {string[]} featureNames The names of known features.
+ */
+ constructor(featureNames) {
+ super();
+ this._featureNames = featureNames;
+ }
+
+ getValue(settingName, roomId) {
+ if (this._featureNames.includes(settingName)) {
+ return this._readFeature(settingName);
+ }
+
+ // Special case notifications
+ if (settingName === "notificationsEnabled") {
+ const value = localStorage.getItem("notifications_enabled");
+ if (typeof(value) === "string") return value === "true";
+ return null; // wrong type or otherwise not set
+ } else if (settingName === "notificationBodyEnabled") {
+ const value = localStorage.getItem("notifications_body_enabled");
+ if (typeof(value) === "string") return value === "true";
+ return null; // wrong type or otherwise not set
+ } else if (settingName === "audioNotificationsEnabled") {
+ const value = localStorage.getItem("audio_notifications_enabled");
+ if (typeof(value) === "string") return value === "true";
+ return null; // wrong type or otherwise not set
+ }
+
+ return this._getSettings()[settingName];
+ }
+
+ setValue(settingName, roomId, newValue) {
+ if (this._featureNames.includes(settingName)) {
+ this._writeFeature(settingName, newValue);
+ return Promise.resolve();
+ }
+
+ // Special case notifications
+ if (settingName === "notificationsEnabled") {
+ localStorage.setItem("notifications_enabled", newValue);
+ return Promise.resolve();
+ } else if (settingName === "notificationBodyEnabled") {
+ localStorage.setItem("notifications_body_enabled", newValue);
+ return Promise.resolve();
+ } else if (settingName === "audioNotificationsEnabled") {
+ localStorage.setItem("audio_notifications_enabled", newValue);
+ return Promise.resolve();
+ }
+
+ const settings = this._getSettings();
+ settings[settingName] = newValue;
+ localStorage.setItem("mx_local_settings", JSON.stringify(settings));
+
+ return Promise.resolve();
+ }
+
+ canSetValue(settingName, roomId) {
+ return true; // It's their device, so they should be able to
+ }
+
+ isSupported() {
+ return localStorage !== undefined && localStorage !== null;
+ }
+
+ _getSettings() {
+ const value = localStorage.getItem("mx_local_settings");
+ if (!value) return {};
+ return JSON.parse(value);
+ }
+
+ // Note: features intentionally don't use the same key as settings to avoid conflicts
+ // and to be backwards compatible.
+
+ _readFeature(featureName) {
+ if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest()) {
+ // Guests should not have any labs features enabled.
+ return {enabled: false};
+ }
+
+ const value = localStorage.getItem("mx_labs_feature_" + featureName);
+ return value === "true";
+ }
+
+ _writeFeature(featureName, enabled) {
+ localStorage.setItem("mx_labs_feature_" + featureName, enabled);
+ }
+}
diff --git a/src/settings/handlers/LocalEchoWrapper.js b/src/settings/handlers/LocalEchoWrapper.js
new file mode 100644
index 0000000000..d616edd9fb
--- /dev/null
+++ b/src/settings/handlers/LocalEchoWrapper.js
@@ -0,0 +1,69 @@
+/*
+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 Promise from "bluebird";
+import SettingsHandler from "./SettingsHandler";
+
+/**
+ * A wrapper for a SettingsHandler that performs local echo on
+ * changes to settings. This wrapper will use the underlying
+ * handler as much as possible to ensure values are not stale.
+ */
+export default class LocalEchoWrapper extends SettingsHandler {
+ /**
+ * Creates a new local echo wrapper
+ * @param {SettingsHandler} handler The handler to wrap
+ */
+ constructor(handler) {
+ super();
+ this._handler = handler;
+ this._cache = {
+ // settingName: { roomId: value }
+ };
+ }
+
+ getValue(settingName, roomId) {
+ const cacheRoomId = roomId ? roomId : "UNDEFINED"; // avoid weird keys
+ const bySetting = this._cache[settingName];
+ if (bySetting && bySetting.hasOwnProperty(cacheRoomId)) {
+ return bySetting[roomId];
+ }
+
+ return this._handler.getValue(settingName, roomId);
+ }
+
+ setValue(settingName, roomId, newValue) {
+ if (!this._cache[settingName]) this._cache[settingName] = {};
+ const bySetting = this._cache[settingName];
+
+ const cacheRoomId = roomId ? roomId : "UNDEFINED"; // avoid weird keys
+ bySetting[cacheRoomId] = newValue;
+
+ const handlerPromise = this._handler.setValue(settingName, roomId, newValue);
+ return Promise.resolve(handlerPromise).finally(() => {
+ delete bySetting[cacheRoomId];
+ });
+ }
+
+
+ canSetValue(settingName, roomId) {
+ return this._handler.canSetValue(settingName, roomId);
+ }
+
+ isSupported() {
+ return this._handler.isSupported();
+ }
+}
diff --git a/src/settings/handlers/RoomAccountSettingsHandler.js b/src/settings/handlers/RoomAccountSettingsHandler.js
new file mode 100644
index 0000000000..e946581807
--- /dev/null
+++ b/src/settings/handlers/RoomAccountSettingsHandler.js
@@ -0,0 +1,83 @@
+/*
+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 SettingsHandler from "./SettingsHandler";
+import MatrixClientPeg from '../../MatrixClientPeg';
+
+/**
+ * Gets and sets settings at the "room-account" level for the current user.
+ */
+export default class RoomAccountSettingsHandler extends SettingsHandler {
+ getValue(settingName, roomId) {
+ // Special case URL previews
+ if (settingName === "urlPreviewsEnabled") {
+ const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
+
+ // Check to make sure that we actually got a boolean
+ if (typeof(content['disable']) !== "boolean") return null;
+ return !content['disable'];
+ }
+
+ // Special case room color
+ if (settingName === "roomColor") {
+ // The event content should already be in an appropriate format, we just need
+ // to get the right value.
+ return this._getSettings(roomId, "org.matrix.room.color_scheme");
+ }
+
+ return this._getSettings(roomId)[settingName];
+ }
+
+ setValue(settingName, roomId, newValue) {
+ // Special case URL previews
+ if (settingName === "urlPreviewsEnabled") {
+ const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
+ content['disable'] = !newValue;
+ return MatrixClientPeg.get().setRoomAccountData(roomId, "org.matrix.room.preview_urls", content);
+ }
+
+ // Special case room color
+ if (settingName === "roomColor") {
+ // The new value should match our requirements, we just need to store it in the right place.
+ return MatrixClientPeg.get().setRoomAccountData(roomId, "org.matrix.room.color_scheme", newValue);
+ }
+
+ const content = this._getSettings(roomId);
+ content[settingName] = newValue;
+ return MatrixClientPeg.get().setRoomAccountData(roomId, "im.vector.web.settings", content);
+ }
+
+ canSetValue(settingName, roomId) {
+ const room = MatrixClientPeg.get().getRoom(roomId);
+
+ // If they have the room, they can set their own account data
+ return room !== undefined && room !== null;
+ }
+
+ isSupported() {
+ const cli = MatrixClientPeg.get();
+ return cli !== undefined && cli !== null;
+ }
+
+ _getSettings(roomId, eventType = "im.vector.settings") {
+ const room = MatrixClientPeg.get().getRoom(roomId);
+ if (!room) return {};
+
+ const event = room.getAccountData(eventType);
+ if (!event || !event.getContent()) return {};
+ return event.getContent();
+ }
+}
diff --git a/src/settings/handlers/RoomDeviceSettingsHandler.js b/src/settings/handlers/RoomDeviceSettingsHandler.js
new file mode 100644
index 0000000000..186be3041f
--- /dev/null
+++ b/src/settings/handlers/RoomDeviceSettingsHandler.js
@@ -0,0 +1,77 @@
+/*
+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 Promise from 'bluebird';
+import SettingsHandler from "./SettingsHandler";
+
+/**
+ * Gets and sets settings at the "room-device" level for the current device in a particular
+ * room.
+ */
+export default class RoomDeviceSettingsHandler extends SettingsHandler {
+ getValue(settingName, roomId) {
+ // Special case blacklist setting to use legacy values
+ if (settingName === "blacklistUnverifiedDevices") {
+ const value = this._read("mx_local_settings");
+ if (value && value['blacklistUnverifiedDevicesPerRoom']) {
+ return value['blacklistUnverifiedDevicesPerRoom'][roomId];
+ }
+ }
+
+ const value = this._read(this._getKey(settingName, roomId));
+ if (value) return value.value;
+ return null;
+ }
+
+ setValue(settingName, roomId, newValue) {
+ // Special case blacklist setting for legacy structure
+ if (settingName === "blacklistUnverifiedDevices") {
+ let value = this._read("mx_local_settings");
+ if (!value) value = {};
+ if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {};
+ value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue;
+ localStorage.setItem("mx_local_settings", JSON.stringify(value));
+ return Promise.resolve();
+ }
+
+ if (newValue === null) {
+ localStorage.removeItem(this._getKey(settingName, roomId));
+ } else {
+ newValue = JSON.stringify({value: newValue});
+ localStorage.setItem(this._getKey(settingName, roomId), newValue);
+ }
+
+ return Promise.resolve();
+ }
+
+ canSetValue(settingName, roomId) {
+ return true; // It's their device, so they should be able to
+ }
+
+ isSupported() {
+ return localStorage !== undefined && localStorage !== null;
+ }
+
+ _read(key) {
+ const rawValue = localStorage.getItem(key);
+ if (!rawValue) return null;
+ return JSON.parse(rawValue);
+ }
+
+ _getKey(settingName, roomId) {
+ return "mx_setting_" + settingName + "_" + roomId;
+ }
+}
diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js
new file mode 100644
index 0000000000..cb3e836c7f
--- /dev/null
+++ b/src/settings/handlers/RoomSettingsHandler.js
@@ -0,0 +1,73 @@
+/*
+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 SettingsHandler from "./SettingsHandler";
+import MatrixClientPeg from '../../MatrixClientPeg';
+
+/**
+ * Gets and sets settings at the "room" level.
+ */
+export default class RoomSettingsHandler extends SettingsHandler {
+ getValue(settingName, roomId) {
+ // Special case URL previews
+ if (settingName === "urlPreviewsEnabled") {
+ const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
+
+ // Check to make sure that we actually got a boolean
+ if (typeof(content['disable']) !== "boolean") return null;
+ return !content['disable'];
+ }
+
+ return this._getSettings(roomId)[settingName];
+ }
+
+ setValue(settingName, roomId, newValue) {
+ // Special case URL previews
+ if (settingName === "urlPreviewsEnabled") {
+ const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
+ content['disable'] = !newValue;
+ return MatrixClientPeg.get().sendStateEvent(roomId, "org.matrix.room.preview_urls", content);
+ }
+
+ const content = this._getSettings(roomId);
+ content[settingName] = newValue;
+ return MatrixClientPeg.get().sendStateEvent(roomId, "im.vector.web.settings", content, "");
+ }
+
+ canSetValue(settingName, roomId) {
+ const cli = MatrixClientPeg.get();
+ const room = cli.getRoom(roomId);
+
+ let eventType = "im.vector.web.settings";
+ if (settingName === "urlPreviewsEnabled") eventType = "org.matrix.room.preview_urls";
+
+ if (!room) return false;
+ return room.currentState.maySendStateEvent(eventType, cli.getUserId());
+ }
+
+ isSupported() {
+ const cli = MatrixClientPeg.get();
+ return cli !== undefined && cli !== null;
+ }
+
+ _getSettings(roomId, eventType = "im.vector.web.settings") {
+ const room = MatrixClientPeg.get().getRoom(roomId);
+ if (!room) return {};
+ const event = room.currentState.getStateEvents(eventType, "");
+ if (!event || !event.getContent()) return {};
+ return event.getContent();
+ }
+}
diff --git a/src/settings/handlers/SettingsHandler.js b/src/settings/handlers/SettingsHandler.js
new file mode 100644
index 0000000000..69f633c650
--- /dev/null
+++ b/src/settings/handlers/SettingsHandler.js
@@ -0,0 +1,71 @@
+/*
+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 Promise from "bluebird";
+
+/**
+ * Represents the base class for all level handlers. This class performs no logic
+ * and should be overridden.
+ */
+export default class SettingsHandler {
+ /**
+ * Gets the value for a particular setting at this level for a particular room.
+ * If no room is applicable, the roomId may be null. The roomId may not be
+ * applicable to this level and may be ignored by the handler.
+ * @param {string} settingName The name of the setting.
+ * @param {String} roomId The room ID to read from, may be null.
+ * @returns {*} The setting value, or null if not found.
+ */
+ getValue(settingName, roomId) {
+ console.error("Invalid operation: getValue was not overridden");
+ return null;
+ }
+
+ /**
+ * Sets the value for a particular setting at this level for a particular room.
+ * If no room is applicable, the roomId may be null. The roomId may not be
+ * applicable to this level and may be ignored by the handler. Setting a value
+ * to null will cause the level to remove the value. The current user should be
+ * able to set the value prior to calling this.
+ * @param {string} settingName The name of the setting to change.
+ * @param {String} roomId The room ID to set the value in, may be null.
+ * @param {*} newValue The new value for the setting, may be null.
+ * @returns {Promise} Resolves when the setting has been saved.
+ */
+ setValue(settingName, roomId, newValue) {
+ console.error("Invalid operation: setValue was not overridden");
+ return Promise.reject();
+ }
+
+ /**
+ * Determines if the current user is able to set the value of the given setting
+ * in the given room at this level.
+ * @param {string} settingName The name of the setting to check.
+ * @param {String} roomId The room ID to check in, may be null
+ * @returns {boolean} True if the setting can be set by the user, false otherwise.
+ */
+ canSetValue(settingName, roomId) {
+ return false;
+ }
+
+ /**
+ * Determines if this level is supported on this device.
+ * @returns {boolean} True if this level is supported on the current device.
+ */
+ isSupported() {
+ return false;
+ }
+}
diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js
index 1501e28875..1ecd1ac051 100644
--- a/src/shouldHideEvent.js
+++ b/src/shouldHideEvent.js
@@ -14,6 +14,8 @@
limitations under the License.
*/
+import SettingsStore from "./settings/SettingsStore";
+
function memberEventDiff(ev) {
const diff = {
isMemberEvent: ev.getType() === 'm.room.member',
@@ -34,16 +36,19 @@ function memberEventDiff(ev) {
return diff;
}
-export default function shouldHideEvent(ev, syncedSettings) {
+export default function shouldHideEvent(ev) {
+ // Wrap getValue() for readability
+ const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId());
+
// Hide redacted events
- if (syncedSettings['hideRedactions'] && ev.isRedacted()) return true;
+ if (isEnabled('hideRedactions') && ev.isRedacted()) return true;
const eventDiff = memberEventDiff(ev);
if (eventDiff.isMemberEvent) {
- if (syncedSettings['hideJoinLeaves'] && (eventDiff.isJoin || eventDiff.isPart)) return true;
- const isMemberAvatarDisplaynameChange = eventDiff.isAvatarChange || eventDiff.isDisplaynameChange;
- if (syncedSettings['hideAvatarDisplaynameChanges'] && isMemberAvatarDisplaynameChange) return true;
+ if (isEnabled('hideJoinLeaves') && (eventDiff.isJoin || eventDiff.isPart)) return true;
+ if (isEnabled('hideAvatarChanges') && eventDiff.isAvatarChange) return true;
+ if (isEnabled('hideDisplaynameChanges') && eventDiff.isDisplaynameChange) return true;
}
return false;
diff --git a/src/stores/FilterStore.js b/src/stores/FilterStore.js
new file mode 100644
index 0000000000..8078a13ffd
--- /dev/null
+++ b/src/stores/FilterStore.js
@@ -0,0 +1,115 @@
+/*
+Copyright 2017 Vector Creations Ltd
+
+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 {Store} from 'flux/utils';
+import dis from '../dispatcher';
+import Analytics from '../Analytics';
+
+const INITIAL_STATE = {
+ allTags: [],
+ selectedTags: [],
+ // Last selected tag when shift was not being pressed
+ anchorTag: null,
+};
+
+/**
+ * A class for storing application state for filtering via TagPanel.
+ */
+class FilterStore extends Store {
+ constructor() {
+ super(dis);
+
+ // Initialise state
+ this._state = INITIAL_STATE;
+ }
+
+ _setState(newState) {
+ this._state = Object.assign(this._state, newState);
+ this.__emitChange();
+ }
+
+ __onDispatch(payload) {
+ switch (payload.action) {
+ case 'all_tags' :
+ this._setState({
+ allTags: payload.tags,
+ });
+ break;
+ case 'select_tag': {
+ let newTags = [];
+ // Shift-click semantics
+ if (payload.shiftKey) {
+ // Select range of tags
+ let start = this._state.allTags.indexOf(this._state.anchorTag);
+ let end = this._state.allTags.indexOf(payload.tag);
+
+ if (start === -1) {
+ start = end;
+ }
+ if (start > end) {
+ const temp = start;
+ start = end;
+ end = temp;
+ }
+ newTags = payload.ctrlOrCmdKey ? this._state.selectedTags : [];
+ newTags = [...new Set(
+ this._state.allTags.slice(start, end + 1).concat(newTags),
+ )];
+ } else {
+ if (payload.ctrlOrCmdKey) {
+ // Toggle individual tag
+ if (this._state.selectedTags.includes(payload.tag)) {
+ newTags = this._state.selectedTags.filter((t) => t !== payload.tag);
+ } else {
+ newTags = [...this._state.selectedTags, payload.tag];
+ }
+ } else {
+ // Select individual tag
+ newTags = [payload.tag];
+ }
+ // Only set the anchor tag if the tag was previously unselected, otherwise
+ // the next range starts with an unselected tag.
+ if (!this._state.selectedTags.includes(payload.tag)) {
+ this._setState({
+ anchorTag: payload.tag,
+ });
+ }
+ }
+
+ this._setState({
+ selectedTags: newTags,
+ });
+
+ Analytics.trackEvent('FilterStore', 'select_tag');
+ }
+ break;
+ case 'deselect_tags':
+ this._setState({
+ selectedTags: [],
+ });
+ Analytics.trackEvent('FilterStore', 'deselect_tags');
+ break;
+ }
+ }
+
+ getSelectedTags() {
+ return this._state.selectedTags;
+ }
+}
+
+if (global.singletonFilterStore === undefined) {
+ global.singletonFilterStore = new FilterStore();
+}
+export default global.singletonFilterStore;
diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js
index 1d20148c0d..55c4978925 100644
--- a/src/stores/FlairStore.js
+++ b/src/stores/FlairStore.js
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import EventEmitter from 'events';
import Promise from 'bluebird';
const BULK_REQUEST_DEBOUNCE_MS = 200;
@@ -29,9 +28,8 @@ const GROUP_PROFILES_CACHE_BUST_MS = 1800000; // 30 mins
/**
* Stores data used by
*/
-class FlairStore extends EventEmitter {
+class FlairStore {
constructor(matrixClient) {
- super();
this._matrixClient = matrixClient;
this._userGroups = {
// $userId: ['+group1:domain', '+group2:domain', ...]
@@ -69,9 +67,13 @@ class FlairStore extends EventEmitter {
}
// Bulk lookup ongoing, return promise to resolve/reject
- if (this._usersPending[userId] || this._usersInFlight[userId]) {
+ if (this._usersPending[userId]) {
return this._usersPending[userId].prom;
}
+ // User has been moved from pending to in-flight
+ if (this._usersInFlight[userId]) {
+ return this._usersInFlight[userId].prom;
+ }
this._usersPending[userId] = {};
this._usersPending[userId].prom = new Promise((resolve, reject) => {
@@ -91,7 +93,7 @@ class FlairStore extends EventEmitter {
// Return silently to avoid spamming for non-supporting servers
return;
}
- console.error('Could not get groups for user', this.props.userId, err);
+ console.error('Could not get groups for user', userId, err);
throw err;
}).finally(() => {
delete this._usersInFlight[userId];
diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js
index 11dd664053..8d95d1975b 100644
--- a/src/stores/GroupStore.js
+++ b/src/stores/GroupStore.js
@@ -17,6 +17,7 @@ limitations under the License.
import EventEmitter from 'events';
import { groupMemberFromApiObject, groupRoomFromApiObject } from '../groups';
import FlairStore from './FlairStore';
+import MatrixClientPeg from '../MatrixClientPeg';
/**
* Stores the group summary for a room and provides an API to change it and
@@ -31,13 +32,12 @@ export default class GroupStore extends EventEmitter {
GroupRooms: 'GroupRooms',
};
- constructor(matrixClient, groupId) {
+ constructor(groupId) {
super();
if (!groupId) {
throw new Error('GroupStore needs a valid groupId to be created');
}
this.groupId = groupId;
- this._matrixClient = matrixClient;
this._summary = {};
this._rooms = [];
this._members = [];
@@ -50,7 +50,7 @@ export default class GroupStore extends EventEmitter {
}
_fetchMembers() {
- this._matrixClient.getGroupUsers(this.groupId).then((result) => {
+ MatrixClientPeg.get().getGroupUsers(this.groupId).then((result) => {
this._members = result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember);
});
@@ -61,7 +61,7 @@ export default class GroupStore extends EventEmitter {
this.emit('error', err);
});
- this._matrixClient.getGroupInvitedUsers(this.groupId).then((result) => {
+ MatrixClientPeg.get().getGroupInvitedUsers(this.groupId).then((result) => {
this._invitedMembers = result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember);
});
@@ -78,7 +78,7 @@ export default class GroupStore extends EventEmitter {
}
_fetchSummary() {
- this._matrixClient.getGroupSummary(this.groupId).then((resp) => {
+ MatrixClientPeg.get().getGroupSummary(this.groupId).then((resp) => {
this._summary = resp;
this._ready[GroupStore.STATE_KEY.Summary] = true;
this._notifyListeners();
@@ -88,7 +88,7 @@ export default class GroupStore extends EventEmitter {
}
_fetchRooms() {
- this._matrixClient.getGroupRooms(this.groupId).then((resp) => {
+ MatrixClientPeg.get().getGroupRooms(this.groupId).then((resp) => {
this._rooms = resp.chunk.map((apiRoom) => {
return groupRoomFromApiObject(apiRoom);
});
@@ -145,19 +145,19 @@ export default class GroupStore extends EventEmitter {
}
addRoomToGroup(roomId, isPublic) {
- return this._matrixClient
+ return MatrixClientPeg.get()
.addRoomToGroup(this.groupId, roomId, isPublic)
.then(this._fetchRooms.bind(this));
}
- updateGroupRoomAssociation(roomId, isPublic) {
- return this._matrixClient
- .updateGroupRoomAssociation(this.groupId, roomId, isPublic)
+ updateGroupRoomVisibility(roomId, isPublic) {
+ return MatrixClientPeg.get()
+ .updateGroupRoomVisibility(this.groupId, roomId, isPublic)
.then(this._fetchRooms.bind(this));
}
removeRoomFromGroup(roomId) {
- return this._matrixClient
+ return MatrixClientPeg.get()
.removeRoomFromGroup(this.groupId, roomId)
// Room might be in the summary, refresh just in case
.then(this._fetchSummary.bind(this))
@@ -165,38 +165,46 @@ export default class GroupStore extends EventEmitter {
}
inviteUserToGroup(userId) {
- return this._matrixClient.inviteUserToGroup(this.groupId, userId)
+ return MatrixClientPeg.get().inviteUserToGroup(this.groupId, userId)
+ .then(this._fetchMembers.bind(this));
+ }
+
+ acceptGroupInvite() {
+ return MatrixClientPeg.get().acceptGroupInvite(this.groupId)
+ // The user might be able to see more rooms now
+ .then(this._fetchRooms.bind(this))
+ // The user should now appear as a member
.then(this._fetchMembers.bind(this));
}
addRoomToGroupSummary(roomId, categoryId) {
- return this._matrixClient
+ return MatrixClientPeg.get()
.addRoomToGroupSummary(this.groupId, roomId, categoryId)
.then(this._fetchSummary.bind(this));
}
addUserToGroupSummary(userId, roleId) {
- return this._matrixClient
+ return MatrixClientPeg.get()
.addUserToGroupSummary(this.groupId, userId, roleId)
.then(this._fetchSummary.bind(this));
}
removeRoomFromGroupSummary(roomId) {
- return this._matrixClient
+ return MatrixClientPeg.get()
.removeRoomFromGroupSummary(this.groupId, roomId)
.then(this._fetchSummary.bind(this));
}
removeUserFromGroupSummary(userId) {
- return this._matrixClient
+ return MatrixClientPeg.get()
.removeUserFromGroupSummary(this.groupId, userId)
.then(this._fetchSummary.bind(this));
}
setGroupPublicity(isPublished) {
- return this._matrixClient
+ return MatrixClientPeg.get()
.setGroupPublicity(this.groupId, isPublished)
- .then(() => { FlairStore.invalidatePublicisedGroups(this._matrixClient.credentials.userId); })
+ .then(() => { FlairStore.invalidatePublicisedGroups(MatrixClientPeg.get().credentials.userId); })
.then(this._fetchSummary.bind(this));
}
}
diff --git a/src/stores/GroupStoreCache.js b/src/stores/GroupStoreCache.js
index df5ffcda5e..8b4286831b 100644
--- a/src/stores/GroupStoreCache.js
+++ b/src/stores/GroupStoreCache.js
@@ -21,14 +21,13 @@ class GroupStoreCache {
this.groupStore = null;
}
- getGroupStore(matrixClient, groupId) {
+ getGroupStore(groupId) {
if (!this.groupStore || this.groupStore.groupId !== groupId) {
// This effectively throws away the reference to any previous GroupStore,
// allowing it to be GCd once the components referencing it have stopped
// referencing it.
- this.groupStore = new GroupStore(matrixClient, groupId);
+ this.groupStore = new GroupStore(groupId);
}
- this.groupStore._fetchSummary();
return this.groupStore;
}
}
diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js
index 11f9d86816..01c521da0c 100644
--- a/src/utils/MegolmExportEncryption.js
+++ b/src/utils/MegolmExportEncryption.js
@@ -116,7 +116,7 @@ export async function decryptMegolmKeyFile(data, password) {
aesKey,
ciphertext,
);
- } catch(e) {
+ } catch (e) {
throw friendlyError('subtleCrypto.decrypt failed: ' + e, cryptoFailMsg());
}
diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js
index 02c413ac83..a0f33f5c39 100644
--- a/src/utils/MultiInviter.js
+++ b/src/utils/MultiInviter.js
@@ -119,7 +119,7 @@ export default class MultiInviter {
let doInvite;
if (this.groupId !== null) {
doInvite = GroupStoreCache
- .getGroupStore(MatrixClientPeg.get(), this.groupId)
+ .getGroupStore(this.groupId)
.inviteUserToGroup(addr);
} else {
doInvite = inviteToRoom(this.roomId, addr);
diff --git a/src/utils/createMatrixClient.js b/src/utils/createMatrixClient.js
index 2d294e262b..b83e254fad 100644
--- a/src/utils/createMatrixClient.js
+++ b/src/utils/createMatrixClient.js
@@ -23,7 +23,7 @@ const localStorage = window.localStorage;
let indexedDB;
try {
indexedDB = window.indexedDB;
-} catch(e) {}
+} catch (e) {}
/**
* Create a new matrix client, with the persistent stores set up appropriately
@@ -39,7 +39,9 @@ try {
* @returns {MatrixClient} the newly-created MatrixClient
*/
export default function createMatrixClient(opts) {
- const storeOpts = {};
+ const storeOpts = {
+ useAuthorizationHeader: true,
+ };
if (localStorage) {
storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index 4de3b7626d..e7176e2c16 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import SettingsStore from "../../../src/settings/SettingsStore";
+
const React = require('react');
const ReactDOM = require("react-dom");
const TestUtils = require('react-addons-test-utils');
@@ -23,7 +25,6 @@ import sinon from 'sinon';
const sdk = require('matrix-react-sdk');
const MessagePanel = sdk.getComponent('structures.MessagePanel');
-import UserSettingsStore from '../../../src/UserSettingsStore';
import MatrixClientPeg from '../../../src/MatrixClientPeg';
const test_utils = require('test-utils');
@@ -59,7 +60,9 @@ describe('MessagePanel', function() {
sandbox = test_utils.stubClient();
client = MatrixClientPeg.get();
client.credentials = {userId: '@me:here'};
- UserSettingsStore.getSyncedSettings = sinon.stub().returns({});
+
+ // HACK: We assume all settings want to be disabled
+ SettingsStore.getValue = sinon.stub().returns(false);
});
afterEach(function() {
diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js
index faf3f0f804..1f0ede6ae2 100644
--- a/test/components/views/rooms/MessageComposerInput-test.js
+++ b/test/components/views/rooms/MessageComposerInput-test.js
@@ -6,7 +6,6 @@ import sinon from 'sinon';
import Promise from 'bluebird';
import * as testUtils from '../../../test-utils';
import sdk from 'matrix-react-sdk';
-import UserSettingsStore from '../../../../src/UserSettingsStore';
const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput');
import MatrixClientPeg from '../../../../src/MatrixClientPeg';
import RoomMember from 'matrix-js-sdk';
diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js
new file mode 100644
index 0000000000..ce9f8e1684
--- /dev/null
+++ b/test/i18n-test/languageHandler-test.js
@@ -0,0 +1,73 @@
+const React = require('react');
+const expect = require('expect');
+import * as languageHandler from '../../src/languageHandler';
+
+const testUtils = require('../test-utils');
+
+describe('languageHandler', function() {
+ let sandbox;
+
+ beforeEach(function(done) {
+ testUtils.beforeEach(this);
+ sandbox = testUtils.stubClient();
+
+ languageHandler.setLanguage('en').done(done);
+ });
+
+ afterEach(function() {
+ sandbox.restore();
+ });
+
+ it('translates a string to german', function() {
+ languageHandler.setLanguage('de').then(function() {
+ const translated = languageHandler._t('Rooms');
+ expect(translated).toBe('Räume');
+ });
+ });
+
+ it('handles plurals', function() {
+ const text = 'and %(count)s others...';
+ expect(languageHandler._t(text, { count: 1 })).toBe('and one other...');
+ expect(languageHandler._t(text, { count: 2 })).toBe('and 2 others...');
+ });
+
+ it('handles simple variable subsitutions', function() {
+ const text = 'You are now ignoring %(userId)s';
+ expect(languageHandler._t(text, { userId: 'foo' })).toBe('You are now ignoring foo');
+ });
+
+ it('handles simple tag substitution', function() {
+ const text = 'Press to start a chat with someone';
+ expect(languageHandler._t(text, {}, { 'StartChatButton': () => 'foo' }))
+ .toBe('Press foo to start a chat with someone');
+ });
+
+ it('handles text in tags', function() {
+ const text = 'Click here to join the discussion!';
+ expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` }))
+ .toBe('xClick herex to join the discussion!');
+ });
+
+ it('variable substitution with React component', function() {
+ const text = 'You are now ignoring %(userId)s';
+ expect(languageHandler._t(text, { userId: () => foo }))
+ .toEqual((You are now ignoring foo));
+ });
+
+ it('variable substitution with plain React component', function() {
+ const text = 'You are now ignoring %(userId)s';
+ expect(languageHandler._t(text, { userId: foo }))
+ .toEqual((You are now ignoring foo));
+ });
+
+ it('tag substitution with React component', function() {
+ const text = 'Press to start a chat with someone';
+ expect(languageHandler._t(text, {}, { 'StartChatButton': () => foo }))
+ .toEqual(Press foo to start a chat with someone);
+ });
+
+ it('replacements in the wrong order', function() {
+ const text = '%(var1)s %(var2)s';
+ expect(languageHandler._t(text, { var2: 'val2', var1: 'val1' })).toBe('val1 val2');
+ });
+});