This commit is contained in:
Peter Vágner 2017-12-06 11:07:01 +01:00
commit 802abe7091
140 changed files with 7437 additions and 2134 deletions

View file

@ -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", {

View file

@ -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)

151
docs/settings.md Normal file
View file

@ -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
<SettingsFlag name="theSettingId"
level={SettingsLevel.ROOM}
roomId="!curbf:matrix.org"
label={_td("Your label here")} // optional, if falsey then the `SettingsStore` will be used
onChange={function(newValue) { }} // optional, called after saving
isExplicit={false} // this is passed along to `SettingsStore.getValueAt`, defaulting to false
manualSave={false} // if true, saving is delayed. You will need to call .save() on this component
// Options for radio buttons
group="your-radio-group" // this enables radio button support
value="yourValueHere" // the value for this particular option
/>
```
### 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"`.

View file

@ -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",

View file

@ -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}`);

View file

@ -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({

View file

@ -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);
},
};

View file

@ -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 = <div>
<div>{ _t("Which rooms would you like to add to this community?") }</div>
<div className="warning">
{ _t(
"Warning: any room you add to a community will be publicly "+
"visible to anyone who knows the community ID",
) }
</div>
</div>;
const checkboxContainer = <label className="mx_GroupAddressPicker_checkboxContainer">
<input type="checkbox" onClick={onCheckboxClicked} />
<div>
{ _t("Show these rooms to non-members on the community page and room list?") }
</div>
</label>;
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;

View file

@ -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;
}
}

View file

@ -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'});
}
/*

View file

@ -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);

View file

@ -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,
@ -93,6 +94,7 @@ 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();
@ -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);

View file

@ -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;

View file

@ -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();
}

View file

@ -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;
}

View file

@ -26,7 +26,7 @@ const DEFAULTS = {
class SdkConfig {
static get() {
return global.mxReactSdkConfig;
return global.mxReactSdkConfig || {};
}
static put(cfg) {

View file

@ -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),
);
}
}

View file

@ -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,17 +15,53 @@ 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 = [
// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
function colorToRgb(color) {
if (!color) {
return [0, 0, 0];
}
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]),
];
}
}
return [0, 0, 0];
}
// 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);
}
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)", // BottomLeftMenu overlay (20% Vector 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
@ -35,32 +72,45 @@ const keyRgb = [
// x = (255 - 234) / (255 - 118) = 0.16
// The colour keys to be replaced as referred to in SVGs
const keyHex = [
this.keyHex = [
"#76CFA6", // Vector Green
"#EAF5F0", // Vector Light Green
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on 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)
];
// cache of our replacement colours
// track the replacement colours actually being used
// defaults to our keys.
const colors = [
keyHex[0],
keyHex[1],
keyHex[2],
keyHex[3],
this.colors = [
this.keyHex[0],
this.keyHex[1],
this.keyHex[2],
this.keyHex[3],
this.keyHex[4],
];
const cssFixups = [
// {
// 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
const cssAttrs = [
this.cssAttrs = [
"color",
"backgroundColor",
"borderColor",
@ -69,15 +119,184 @@ const cssAttrs = [
"borderLeftColor",
];
const svgAttrs = [
this.svgAttrs = [
"fill",
"stroke",
];
let cached = false;
// 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;
}
/**
* Register a callback to fire when the tint changes.
* This is used to rewrite the tintable SVGs with the new tint.
*
* It's not possible to unregister a tintable callback. So this can only be
* used to register a static callback. If a set of tintables will change
* over time then the best bet is to register a single callback for the
* entire set.
*
* @param {Function} tintable Function to call when the tint changes.
*/
registerTintable(tintable) {
this.tintables.push(tintable);
}
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 = 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 = 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 = rgbToColor(rgb);
}
if (!tertiaryColor) {
const x = 0.19;
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 = rgbToColor(rgb1);
}
if (this.forceTint == false &&
this.colors[0] === primaryColor &&
this.colors[1] === secondaryColor &&
this.colors[2] === tertiaryColor) {
return;
}
this.forceTint = false;
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.
this.applyCssFixups();
// tell all the SVGs to go fix themselves up
// we don't do this as a dispatch otherwise it will visually lag
this.tintables.forEach(function(tintable) {
tintable();
});
}
tintSvgWhite(whiteColor) {
this.currentTint[3] = whiteColor;
if (!whiteColor) {
whiteColor = this.colors[3];
}
if (this.colors[3] === whiteColor) {
return;
}
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] = [];
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 >:(
@ -100,18 +319,29 @@ function calcCssFixups() {
// 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
if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue;
// 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;
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({
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,
@ -121,125 +351,37 @@ function calcCssFixups() {
}
}
}
if (DEBUG) console.log("calcSvgFixups end");
if (DEBUG) {
console.log("calcCssFixups end (" +
this.cssFixups[this.theme].length +
" fixups)");
}
}
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];
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");
}
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) {
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 = [];
module.exports = {
/**
* Register a callback to fire when the tint changes.
* This is used to rewrite the tintable SVGs with the new tint.
*
* It's not possible to unregister a tintable callback. So this can only be
* used to register a static callback. If a set of tintables will change
* over time then the best bet is to register a single callback for the
* entire set.
*
* @param {Function} tintable Function to call when the tint changes.
*/
registerTintable: function(tintable) {
tintables.push(tintable);
},
tint: function(primaryColor, secondaryColor, tertiaryColor) {
if (!cached) {
calcCssFixups();
cached = true;
}
if (!primaryColor) {
primaryColor = "#76CFA6"; // Vector green
secondaryColor = "#EAF5F0"; // Vector light green
}
if (!secondaryColor) {
const x = 0.16; // average weighting factor calculated from vector green & light green
const rgb = hexToRgb(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);
}
if (!tertiaryColor) {
const x = 0.19;
const rgb1 = hexToRgb(primaryColor);
const rgb2 = hexToRgb(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);
}
if (colors[0] === primaryColor &&
colors[1] === secondaryColor &&
colors[2] === tertiaryColor) {
return;
}
colors[0] = primaryColor;
colors[1] = secondaryColor;
colors[2] = tertiaryColor;
if (DEBUG) console.log("Tinter.tint");
// go through manually fixing up the stylesheets.
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) {
tintable();
});
},
tintSvgWhite: function(whiteColor) {
if (!whiteColor) {
whiteColor = colors[3];
}
if (colors[3] === whiteColor) {
return;
}
colors[3] = whiteColor;
tintables.forEach(function(tintable) {
tintable();
});
},
// 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,7 +390,7 @@ 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) {
@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);
},
};

326
src/WidgetMessaging.js Normal file
View file

@ -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: <Original Error Object>
}
}
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,
};

View file

@ -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
}

View file

@ -53,9 +53,11 @@ export default class UserProvider extends AutocompleteProvider {
}
destroy() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
}
}
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
if (!room) return;

View file

@ -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 = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
position.left = props.left;
} else {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>;
position.right = props.right;
}
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace}></div>;
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 = {};

View file

@ -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 <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">
{ _tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{ sub }</a>) }
{ _t("You must <a>register</a> to use this functionality",
{},
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
}
</div>
</div>;
} else if (this.noRoom) {

View file

@ -22,7 +22,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index';
import dis from '../../dispatcher';
import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t, _td, _tJsx } from '../../languageHandler';
import { _t, _td } from '../../languageHandler';
import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal';
import classnames from 'classnames';
@ -430,18 +430,20 @@ export default React.createClass({
uploadingAvatar: false,
membershipBusy: false,
publicityBusy: false,
inviterProfile: null,
};
},
componentWillMount: function() {
this._matrixClient = MatrixClientPeg.get();
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
this._changeAvatarComponent = null;
this._initGroupStore(this.props.groupId, true);
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
},
componentWillUnmount: function() {
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
this._groupStore.removeAllListeners();
},
@ -463,7 +465,11 @@ export default React.createClass({
},
_initGroupStore: function(groupId, firstInit) {
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
const group = this._matrixClient.getGroup(groupId);
if (group && group.inviter && group.inviter.userId) {
this._fetchInviterProfile(group.inviter.userId);
}
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
const summary = this._groupStore.getSummary();
if (summary.profile) {
@ -481,7 +487,7 @@ export default React.createClass({
groupRooms: this._groupStore.getGroupRooms(),
groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
isUserMember: this._groupStore.getGroupMembers().some(
(m) => m.userId === MatrixClientPeg.get().credentials.userId,
(m) => m.userId === this._matrixClient.credentials.userId,
),
error: null,
});
@ -489,7 +495,19 @@ export default React.createClass({
this._onEditClick();
}
});
let willDoOnboarding = false;
this._groupStore.on('error', (err) => {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
dis.dispatch({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'view_group',
group_id: groupId,
},
});
dis.dispatch({action: 'view_set_mxid'});
willDoOnboarding = true;
}
this.setState({
summary: null,
error: err,
@ -497,6 +515,26 @@ export default React.createClass({
});
},
_fetchInviterProfile(userId) {
this.setState({
inviterProfileBusy: true,
});
this._matrixClient.getProfileInfo(userId).then((resp) => {
this.setState({
inviterProfile: {
avatarUrl: resp.avatar_url,
displayName: resp.displayname,
},
});
}).catch((e) => {
console.error('Error getting group inviter profile', e);
}).finally(() => {
this.setState({
inviterProfileBusy: false,
});
});
},
_onShowRhsClick: function(ev) {
dis.dispatch({ action: 'show_right_panel' });
},
@ -546,7 +584,7 @@ export default React.createClass({
if (!file) return;
this.setState({uploadingAvatar: true});
MatrixClientPeg.get().uploadContent(file).then((url) => {
this._matrixClient.uploadContent(file).then((url) => {
const newProfileForm = Object.assign(this.state.profileForm, { avatar_url: url });
this.setState({
uploadingAvatar: false,
@ -566,7 +604,7 @@ export default React.createClass({
_onSaveClick: function() {
this.setState({saving: true});
const savePromise = this.state.isUserPrivileged ?
MatrixClientPeg.get().setGroupProfile(this.props.groupId, this.state.profileForm) :
this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm) :
Promise.resolve();
savePromise.then((result) => {
this.setState({
@ -591,7 +629,7 @@ export default React.createClass({
_onAcceptInviteClick: function() {
this.setState({membershipBusy: true});
MatrixClientPeg.get().acceptGroupInvite(this.props.groupId).then(() => {
this._groupStore.acceptGroupInvite().then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -605,7 +643,7 @@ export default React.createClass({
_onRejectInviteClick: function() {
this.setState({membershipBusy: true});
MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => {
this._matrixClient.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -628,7 +666,7 @@ export default React.createClass({
if (!confirmed) return;
this.setState({membershipBusy: true});
MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => {
this._matrixClient.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -646,18 +684,6 @@ export default React.createClass({
showGroupAddRoomDialog(this.props.groupId);
},
_onPublicityToggle: function() {
this.setState({
publicityBusy: true,
});
const publicity = !this.state.isGroupPublicised;
this._groupStore.setGroupPublicity(publicity).then(() => {
this.setState({
publicityBusy: false,
});
});
},
_getGroupSection: function() {
const groupSettingsSectionClasses = classnames({
"mx_GroupView_group": this.state.editing,
@ -802,20 +828,37 @@ export default React.createClass({
_getMembershipSection: function() {
const Spinner = sdk.getComponent("elements.Spinner");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const group = MatrixClientPeg.get().getGroup(this.props.groupId);
const group = this._matrixClient.getGroup(this.props.groupId);
if (!group) return null;
if (group.myMembership === 'invite') {
if (this.state.membershipBusy) {
if (this.state.membershipBusy || this.state.inviterProfileBusy) {
return <div className="mx_GroupView_membershipSection">
<Spinner />
</div>;
}
const httpInviterAvatar = this.state.inviterProfile ?
this._matrixClient.mxcUrlToHttp(
this.state.inviterProfile.avatarUrl, 36, 36,
) : null;
let inviterName = group.inviter.userId;
if (this.state.inviterProfile) {
inviterName = this.state.inviterProfile.displayName || group.inviter.userId;
}
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited">
<div className="mx_GroupView_membershipSubSection">
<div className="mx_GroupView_membershipSection_description">
{ _t("%(inviter)s has invited you to join this community", {inviter: group.inviter.userId}) }
<BaseAvatar url={httpInviterAvatar}
name={inviterName}
width={36}
height={36}
/>
{ _t("%(inviter)s has invited you to join this community", {
inviter: inviterName,
}) }
</div>
<div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
@ -860,25 +903,6 @@ export default React.createClass({
return null;
},
_getMemberSettingsSection: function() {
return <div className="mx_GroupView_memberSettings">
<h2> { _t("Community Member Settings") } </h2>
<div className="mx_GroupView_memberSettings_toggle">
<input type="checkbox"
onClick={this._onPublicityToggle}
checked={this.state.isGroupPublicised}
tabIndex="3"
id="isGroupPublicised"
/>
<label htmlFor="isGroupPublicised"
onClick={this._onPublicityToggle}
>
{ _t("Publish this community on your profile") }
</label>
</div>
</div>;
},
_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.<br />' +
'Click here to open settings and give it one!',
[/<br \/>/],
[(sub) => <br />])
}
{},
{ 'br': <br /> },
) }
</div>;
}
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 = [];

View file

@ -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({
<div className='mx_MatrixChat_wrapper'>
{ topBar }
<div className={bodyClasses}>
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
<LeftPanel
selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapseLhs || false}

View file

@ -22,7 +22,6 @@ import React from 'react';
import Matrix from "matrix-js-sdk";
import Analytics from "../../Analytics";
import UserSettingsStore from '../../UserSettingsStore';
import MatrixClientPeg from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg";
import SdkConfig from "../../SdkConfig";
@ -44,6 +43,7 @@ import createRoom from "../../createRoom";
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
/** constants for MatrixChat.state.view */
const VIEWS = {
@ -74,6 +74,16 @@ const VIEWS = {
LOGGED_IN: 6,
};
// Actions that are redirected through the onboarding process prior to being
// re-dispatched. NOTE: some actions are non-trivial and would require
// re-factoring to be included in this list in future.
const ONBOARDING_FLOW_STARTERS = [
'view_user_settings',
'view_create_chat',
'view_create_room',
'view_create_group',
];
module.exports = React.createClass({
// we export this so that the integration tests can use it :-S
statics: {
@ -213,7 +223,7 @@ module.exports = React.createClass({
componentWillMount: function() {
SdkConfig.put(this.props.config);
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
if (!SettingsStore.getValue("analyticsOptOut")) Analytics.enable();
// Used by _viewRoom before getting state from sync
this.firstSyncComplete = false;
@ -276,6 +286,11 @@ module.exports = React.createClass({
this._windowWidth = 10000;
this.handleResize();
window.addEventListener('resize', this.handleResize);
// check we have the right tint applied for this theme.
// N.B. we don't call the whole of setTheme() here as we may be
// racing with the theme CSS download finishing from index.js
Tinter.tint();
},
componentDidMount: function() {
@ -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) {

View file

@ -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() {

View file

@ -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 <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
<div className="mx_GroupTile_avatar">
<BaseAvatar name={name} url={httpUrl} width={50} height={50} />
</div>
<div className="mx_GroupTile_profile">
<h3 className="mx_GroupTile_name">{ name }</h3>
<div className="mx_GroupTile_desc">{ desc }</div>
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
</div>
</AccessibleButton>;
},
});
export default withMatrixClient(React.createClass({
displayName: 'MyGroups',
@ -98,14 +41,18 @@ export default withMatrixClient(React.createClass({
},
_onCreateGroupClick: function() {
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
dis.dispatch({action: 'view_create_group'});
},
_fetch: function() {
this.props.matrixClient.getJoinedGroups().done((result) => {
this.setState({groups: result.groups, error: null});
}, (err) => {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
// Indicate that the guest isn't in any groups (which should be true)
this.setState({groups: [], error: null});
return;
}
this.setState({groups: null, error: err});
});
},
@ -114,6 +61,7 @@ export default withMatrixClient(React.createClass({
const Loader = sdk.getComponent("elements.Spinner");
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const GroupTile = sdk.getComponent("groups.GroupTile");
let content;
let contentHeader;
@ -165,13 +113,13 @@ export default withMatrixClient(React.createClass({
<div className="mx_MyGroups_headerCard_header">
{ _t('Join an existing community') }
</div>
{ _tJsx(
{ _t(
'To join an existing community you\'ll have to '+
'know its community identifier; this will look '+
'something like <i>+example:matrix.org</i>.',
/<i>(.*)<\/i>/,
(sub) => <i>{ sub }</i>,
) }
{},
{ 'i': (sub) => <i>{ sub }</i> })
}
</div>
</div>
</div>

View file

@ -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 }
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
{ _tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
[
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this.props.onResendAllClick}>{ sub }</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this.props.onCancelAllClick}>{ sub }</a>,
],
{
_t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
"You can also select individual messages to resend or cancel.",
{},
{
'resendText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this.props.onResendAllClick}>{ sub }</a>,
'cancelText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this.props.onCancelAllClick}>{ sub }</a>,
},
) }
</div>
</div>
@ -322,12 +325,15 @@ module.exports = React.createClass({
if (this.props.sentMessageAndIsAlone) {
return (
<div className="mx_RoomStatusBar_isAlone">
{ _tJsx("There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?",
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
[
(sub) => <a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
],
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
"or <nowarnText>stop warning about the empty room</nowarnText>?",
{},
{
'inviteText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
'nowarnText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
},
) }
</div>
);

View file

@ -29,7 +29,6 @@ const classNames = require("classnames");
const Matrix = require("matrix-js-sdk");
import { _t } from '../../languageHandler';
const UserSettingsStore = require('../../UserSettingsStore');
const MatrixClientPeg = require("../../MatrixClientPeg");
const ContentMessages = require("../../ContentMessages");
const Modal = require("../../Modal");
@ -42,10 +41,11 @@ const rate_limited_func = require('../../ratelimitedfunc');
const ObjectUtils = require('../../ObjectUtils');
const Rooms = require('../../Rooms');
import KeyCode from '../../KeyCode';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import SettingsStore from "../../settings/SettingsStore";
const DEBUG = false;
let debuglog = function() {};
@ -149,8 +149,6 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
MatrixClientPeg.get().on("accountData", this.onAccountData);
this._syncedSettings = UserSettingsStore.getSyncedSettings();
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true);
@ -305,7 +303,7 @@ module.exports = React.createClass({
// Check if user has previously chosen to hide the app drawer for this
// room. If so, do not show apps
let hideWidgetDrawer = localStorage.getItem(
const hideWidgetDrawer = localStorage.getItem(
room.roomId + "_hide_widget_drawer");
if (hideWidgetDrawer === "true") {
@ -435,13 +433,7 @@ module.exports = React.createClass({
onKeyDown: function(ev) {
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.KEY_D:
@ -542,7 +534,7 @@ module.exports = React.createClass({
// update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change
} else if (!shouldHideEvent(ev, this._syncedSettings)) {
} else if (!shouldHideEvent(ev)) {
this.setState((state, props) => {
return {numUnreadMessages: state.numUnreadMessages + 1};
});
@ -616,38 +608,8 @@ module.exports = React.createClass({
},
_updatePreviewUrlVisibility: function(room) {
// console.log("_updatePreviewUrlVisibility");
// check our per-room overrides
const roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls");
if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) {
this.setState({
showUrlPreview: !roomPreviewUrls.getContent().disable,
});
return;
}
// check our global disable override
const userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) {
this.setState({
showUrlPreview: false,
});
return;
}
// check the room state event
const roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) {
this.setState({
showUrlPreview: false,
});
return;
}
// otherwise, we assume they're on.
this.setState({
showUrlPreview: true,
showUrlPreview: SettingsStore.getValue("urlPreviewsEnabled", room.roomId),
});
},
@ -666,12 +628,7 @@ module.exports = React.createClass({
const room = this.state.room;
if (!room) return;
const color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
let color_scheme = {};
if (color_scheme_event) {
color_scheme = color_scheme_event.getContent();
// XXX: we should validate the event
}
const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
console.log("Tinter.tint from updateTint");
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
},
@ -750,7 +707,7 @@ module.exports = React.createClass({
return;
}
const joinedMembers = room.currentState.getMembers().filter(m => m.membership === "join" || m.membership === "invite");
const joinedMembers = room.currentState.getMembers().filter((m) => m.membership === "join" || m.membership === "invite");
this.setState({isAlone: joinedMembers.length === 1});
},
@ -1775,7 +1732,7 @@ module.exports = React.createClass({
const messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
showReadReceipts={!SettingsStore.getValue('hideReadReceipts')}
manageReadReceipts={!this.state.isPeeking}
manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel}

View file

@ -18,7 +18,7 @@ const React = require("react");
const ReactDOM = require("react-dom");
const GeminiScrollbar = require('react-gemini-scrollbar');
import Promise from 'bluebird';
const KeyCode = require('../../KeyCode');
import { KeyCode } from '../../Keyboard';
const DEBUG_SCROLL = false;
// var DEBUG_SCROLL = true;

View file

@ -0,0 +1,176 @@
/*
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 { MatrixClient } from 'matrix-js-sdk';
import classNames from 'classnames';
import FilterStore from '../../stores/FilterStore';
import FlairStore from '../../stores/FlairStore';
import sdk from '../../index';
import dis from '../../dispatcher';
import { isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
const TagTile = React.createClass({
displayName: 'TagTile',
propTypes: {
groupProfile: PropTypes.object,
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
return {
hover: false,
};
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'select_tag',
tag: this.props.groupProfile.groupId,
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
shiftKey: e.shiftKey,
});
},
onMouseOver: function() {
this.setState({hover: true});
},
onMouseOut: function() {
this.setState({hover: false});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
const profile = this.props.groupProfile || {};
const name = profile.name || profile.groupId;
const avatarHeight = 35;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
const className = classNames({
mx_TagTile: true,
mx_TagTile_selected: this.props.selected,
});
const tip = this.state.hover ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
return <AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
{ tip }
</div>
</AccessibleButton>;
},
});
export default React.createClass({
displayName: 'TagPanel',
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getInitialState() {
return {
joinedGroupProfiles: [],
selectedTags: [],
};
},
componentWillMount: function() {
this.unmounted = false;
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
this._filterStoreToken = FilterStore.addListener(() => {
if (this.unmounted) {
return;
}
this.setState({
selectedTags: FilterStore.getSelectedTags(),
});
});
this._fetchJoinedRooms();
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
if (this._filterStoreToken) {
this._filterStoreToken.remove();
}
},
_onGroupMyMembership() {
if (this.unmounted) return;
this._fetchJoinedRooms();
},
onClick() {
dis.dispatch({action: 'deselect_tags'});
},
onCreateGroupClick(ev) {
ev.stopPropagation();
dis.dispatch({action: 'view_create_group'});
},
async _fetchJoinedRooms() {
const joinedGroupResponse = await this.context.matrixClient.getJoinedGroups();
const joinedGroupIds = joinedGroupResponse.groups;
const joinedGroupProfiles = await Promise.all(joinedGroupIds.map(
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
));
dis.dispatch({
action: 'all_tags',
tags: joinedGroupIds,
});
this.setState({joinedGroupProfiles});
},
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const tags = this.state.joinedGroupProfiles.map((groupProfile, index) => {
return <TagTile
key={groupProfile.groupId + '_' + index}
groupProfile={groupProfile}
selected={this.state.selectedTags.includes(groupProfile.groupId)}
/>;
});
return <div className="mx_TagPanel" onClick={this.onClick}>
<div className="mx_TagPanel_tagTileContainer">
{ tags }
</div>
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
</AccessibleButton>
</div>;
},
});

View file

@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsStore from "../../settings/SettingsStore";
const React = require('react');
const ReactDOM = require("react-dom");
import Promise from 'bluebird';
@ -29,8 +31,7 @@ const dis = require("../../dispatcher");
const ObjectUtils = require('../../ObjectUtils');
const Modal = require("../../Modal");
const UserActivity = require("../../UserActivity");
const KeyCode = require('../../KeyCode');
import UserSettingsStore from '../../UserSettingsStore';
import { KeyCode } from '../../Keyboard';
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@ -129,8 +130,6 @@ var TimelinePanel = React.createClass({
}
}
const syncedSettings = UserSettingsStore.getSyncedSettings();
return {
events: [],
timelineLoading: true, // track whether our room timeline is loading
@ -175,10 +174,10 @@ var TimelinePanel = React.createClass({
clientSyncState: MatrixClientPeg.get().getSyncState(),
// should the event tiles have twelve hour times
isTwelveHour: syncedSettings.showTwelveHourTimestamps,
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
// always show timestamps on event tiles?
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
};
},

View file

@ -15,6 +15,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
const React = require('react');
const ReactDOM = require('react-dom');
const sdk = require('../../index');
@ -56,133 +58,69 @@ const gHVersionLabel = function(repo, token='') {
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
};
// Enumerate some simple 'flip a bit' UI settings (if any).
// 'id' gives the key name in the im.vector.web.settings account data event
// 'label' is how we describe it in the UI.
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
// since they will be translated when rendered.
const SETTINGS_LABELS = [
{
id: 'autoplayGifsAndVideos',
label: _td('Autoplay GIFs and videos'),
},
{
id: 'hideReadReceipts',
label: _td('Hide read receipts'),
},
{
id: 'dontSendTypingNotifications',
label: _td("Don't send typing notifications"),
},
{
id: 'alwaysShowTimestamps',
label: _td('Always show message timestamps'),
},
{
id: 'showTwelveHourTimestamps',
label: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
},
{
id: 'hideJoinLeaves',
label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
},
{
id: 'hideAvatarDisplaynameChanges',
label: _td('Hide avatar and display name changes'),
},
{
id: 'useCompactLayout',
label: _td('Use compact timeline layout'),
},
{
id: 'hideRedactions',
label: _td('Hide removed messages'),
},
{
id: 'enableSyntaxHighlightLanguageDetection',
label: _td('Enable automatic language detection for syntax highlighting'),
},
{
id: 'MessageComposerInput.autoReplaceEmoji',
label: _td('Automatically replace plain text Emoji'),
},
{
id: 'MessageComposerInput.dontSuggestEmoji',
label: _td('Disable Emoji suggestions while typing'),
},
{
id: 'Pill.shouldHidePillAvatar',
label: _td('Hide avatars in user and room mentions'),
},
{
id: 'TextualBody.disableBigEmoji',
label: _td('Disable big emoji in chat'),
},
{
id: 'VideoView.flipVideoHorizontally',
label: _td('Mirror local video feed'),
},
/*
{
id: 'useFixedWidthFont',
label: 'Use fixed width font',
},
*/
// Enumerate some simple 'flip a bit' UI settings (if any). The strings provided here
// must be settings defined in SettingsStore.
const SIMPLE_SETTINGS = [
{ id: "urlPreviewsEnabled" },
{ id: "autoplayGifsAndVideos" },
{ id: "hideReadReceipts" },
{ id: "dontSendTypingNotifications" },
{ id: "alwaysShowTimestamps" },
{ id: "showTwelveHourTimestamps" },
{ id: "hideJoinLeaves" },
{ id: "hideAvatarChanges" },
{ id: "hideDisplaynameChanges" },
{ id: "useCompactLayout" },
{ id: "hideRedactions" },
{ id: "enableSyntaxHighlightLanguageDetection" },
{ id: "MessageComposerInput.autoReplaceEmoji" },
{ id: "MessageComposerInput.dontSuggestEmoji" },
{ id: "Pill.shouldHidePillAvatar" },
{ id: "TextualBody.disableBigEmoji" },
{ id: "VideoView.flipVideoHorizontally" },
];
const ANALYTICS_SETTINGS_LABELS = [
// These settings must be defined in SettingsStore
const ANALYTICS_SETTINGS = [
{
id: 'analyticsOptOut',
label: _td('Opt out of analytics'),
fn: function(checked) {
Analytics[checked ? 'disable' : 'enable']();
},
},
];
const WEBRTC_SETTINGS_LABELS = [
// These settings must be defined in SettingsStore
const WEBRTC_SETTINGS = [
{
id: 'webRtcForceTURN',
label: _td('Disable Peer-to-Peer for 1:1 calls'),
fn: (val) => {
MatrixClientPeg.get().setForceTURN(val);
},
},
];
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
// since they will be translated when rendered.
const CRYPTO_SETTINGS_LABELS = [
// These settings must be defined in SettingsStore
const CRYPTO_SETTINGS = [
{
id: 'blacklistUnverifiedDevices',
label: _td('Never send encrypted messages to unverified devices from this device'),
fn: function(checked) {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
},
},
// XXX: this is here for documentation; the actual setting is managed via RoomSettings
// {
// id: 'blacklistUnverifiedDevicesPerRoom'
// label: 'Never send encrypted messages to unverified devices in this room',
// }
];
// Enumerate the available themes, with a nice human text label.
// 'id' gives the key name in the im.vector.web.settings account data event
// 'value' is the value for that key in the event
// 'label' is how we describe it in the UI.
// 'value' is the value for the theme setting
//
// XXX: Ideally we would have a theme manifest or something and they'd be nicely
// packaged up in a single directory, and/or located at the application layer.
// But for now for expedience we just hardcode them here.
const THEMES = [
{
id: 'theme',
label: _td('Light theme'),
value: 'light',
},
{
id: 'theme',
label: _td('Dark theme'),
value: 'dark',
},
{ label: _td('Light theme'), value: 'light' },
{ label: _td('Dark theme'), value: 'dark' },
{ label: _td('Status.im theme'), value: 'status' },
];
const IgnoredUser = React.createClass({
@ -204,7 +142,7 @@ const IgnoredUser = React.createClass({
render: function() {
return (
<li>
<AccessibleButton onClick={this._onUnignoreClick} className="mx_UserSettings_button mx_UserSettings_buttonSmall">
<AccessibleButton onClick={this._onUnignoreClick} className="mx_textButton">
{ _t("Unignore") }
</AccessibleButton>
{ this.props.userId }
@ -281,14 +219,6 @@ module.exports = React.createClass({
});
this._refreshFromServer();
const syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) {
syncedSettings.theme = 'light';
}
this._syncedSettings = syncedSettings;
this._localSettings = UserSettingsStore.getLocalSettings();
if (PlatformPeg.get().isElectron()) {
const {ipcRenderer} = require('electron');
@ -359,8 +289,8 @@ module.exports = React.createClass({
if (this._unmounted) return;
this.setState({
mediaDevices,
activeAudioInput: this._localSettings['webrtc_audioinput'],
activeVideoInput: this._localSettings['webrtc_videoinput'],
activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
});
});
},
@ -492,10 +422,6 @@ module.exports = React.createClass({
dis.dispatch({action: 'password_changed'});
},
onEnableNotificationsChange: function(event) {
UserSettingsStore.setEnableNotifications(event.target.checked);
},
_onAddEmailEditFinished: function(value, shouldSubmit) {
if (!shouldSubmit) return;
this._addEmail();
@ -669,6 +595,11 @@ module.exports = React.createClass({
});
},
_renderGroupSettings: function() {
const GroupUserSettings = sdk.getComponent('groups.GroupUserSettings');
return <GroupUserSettings />;
},
_renderReferral: function() {
const teamToken = this.props.teamToken;
if (!teamToken) {
@ -692,7 +623,7 @@ module.exports = React.createClass({
onLanguageChange: function(newLang) {
if (this.state.language !== newLang) {
UserSettingsStore.setLocalSetting('language', newLang);
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
this.setState({
language: newLang,
});
@ -715,14 +646,13 @@ module.exports = React.createClass({
// 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('autocompleteDelay', + e.target.value);
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
return (
<div>
<h3>{ _t("User Interface") }</h3>
<div className="mx_UserSettings_section">
{ this._renderUrlPreviewSelector() }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
{ THEMES.map( this._renderThemeSelector ) }
{ SIMPLE_SETTINGS.map( this._renderAccountSetting ) }
{ THEMES.map( this._renderThemeOption ) }
<table>
<tbody>
<tr>
@ -730,7 +660,7 @@ module.exports = React.createClass({
<td>
<input
type="number"
defaultValue={UserSettingsStore.getLocalSetting('autocompleteDelay', 200)}
defaultValue={SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay")}
onChange={onChange}
/>
</td>
@ -743,69 +673,31 @@ module.exports = React.createClass({
);
},
_renderUrlPreviewSelector: function() {
return <div className="mx_UserSettings_toggle">
<input id="urlPreviewsDisabled"
type="checkbox"
defaultChecked={UserSettingsStore.getUrlPreviewsDisabled()}
onChange={this._onPreviewsDisabledChanged}
/>
<label htmlFor="urlPreviewsDisabled">
{ _t("Disable inline URL previews by default") }
</label>
</div>;
_renderAccountSetting: function(setting) {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
return (
<div className="mx_UserSettings_toggle" key={setting.id}>
<SettingsFlag name={setting.id}
label={setting.label}
level={SettingLevel.ACCOUNT}
onChange={setting.fn} />
</div>
);
},
_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 <div className="mx_UserSettings_toggle" key={setting.id}>
<input id={setting.id}
type="checkbox"
defaultChecked={this._syncedSettings[setting.id]}
_renderThemeOption: function(setting) {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const onChange = (v) => dis.dispatch({action: 'set_theme', value: setting.value});
return (
<div className="mx_UserSettings_toggle" key={setting.id + '_' + setting.value}>
<SettingsFlag name="theme"
label={setting.label}
level={SettingLevel.ACCOUNT}
onChange={onChange}
/>
<label htmlFor={setting.id}>
{ _t(setting.label) }
</label>
</div>;
},
_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 <div className="mx_UserSettings_toggle" key={setting.id + "_" + setting.value}>
<input id={setting.id + "_" + setting.value}
type="radio"
name={setting.id}
value={setting.value}
checked={this._syncedSettings[setting.id] === setting.value}
onChange={onChange}
/>
<label htmlFor={setting.id + "_" + setting.value}>
{ _t(setting.label) }
</label>
</div>;
group="theme"
value={setting.value} />
</div>
);
},
_renderCryptoInfo: function() {
@ -847,7 +739,7 @@ module.exports = React.createClass({
{ importExportButtons }
</div>
<div className="mx_UserSettings_section">
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
{ CRYPTO_SETTINGS.map( this._renderDeviceSetting ) }
</div>
</div>
);
@ -873,24 +765,16 @@ module.exports = React.createClass({
} else return (<div />);
},
_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 <div className="mx_UserSettings_toggle" key={setting.id}>
<input id={setting.id}
type="checkbox"
defaultChecked={this._localSettings[setting.id]}
onChange={onChange}
/>
<label htmlFor={setting.id}>
{ _t(setting.label) }
</label>
</div>;
_renderDeviceSetting: function(setting) {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
return (
<div className="mx_UserSettings_toggle" key={setting.id}>
<SettingsFlag name={setting.id}
label={setting.label}
level={SettingLevel.DEVICE}
onChange={setting.fn} />
</div>
);
},
_renderDevicesPanel: function() {
@ -927,18 +811,18 @@ module.exports = React.createClass({
<h3>{ _t('Analytics') }</h3>
<div className="mx_UserSettings_section">
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
{ ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting ) }
{ ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
</div>
</div>;
},
_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}
/>
<label htmlFor={featureId}>{ UserSettingsStore.translatedNameForFeature(featureId) }</label>
<label htmlFor={featureId}>{ SettingsStore.getDisplayName(featureId) }</label>
</div>);
});
@ -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 <div>
<h3>{ _t('Desktop specific') }</h3>
<div className="mx_UserSettings_section">
@ -1166,7 +1052,7 @@ module.exports = React.createClass({
return <div>
<h3>{ _t('VoIP') }</h3>
<div className="mx_UserSettings_section">
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) }
{ WEBRTC_SETTINGS.map(this._renderDeviceSetting) }
{ this._renderWebRtcDeviceSettings() }
</div>
</div>;
@ -1368,6 +1254,8 @@ module.exports = React.createClass({
{ accountJsx }
</div>
{ this._renderGroupSettings() }
{ this._renderReferral() }
{ notificationArea }

View file

@ -17,13 +17,13 @@ limitations under the License.
'use strict';
const React = require('react');
import React from 'react';
import { _t } from '../../../languageHandler';
const sdk = require('../../../index');
const Modal = require("../../../Modal");
const MatrixClientPeg = require('../../../MatrixClientPeg');
import sdk from '../../../index';
import Modal from "../../../Modal";
import MatrixClientPeg from "../../../MatrixClientPeg";
const PasswordReset = require("../../../PasswordReset");
import PasswordReset from "../../../PasswordReset";
module.exports = React.createClass({
displayName: 'ForgotPassword',
@ -154,6 +154,7 @@ module.exports = React.createClass({
},
render: function() {
const LoginPage = sdk.getComponent("login.LoginPage");
const LoginHeader = sdk.getComponent("login.LoginHeader");
const LoginFooter = sdk.getComponent("login.LoginFooter");
const ServerConfig = sdk.getComponent("login.ServerConfig");
@ -165,7 +166,7 @@ module.exports = React.createClass({
resetPasswordJsx = <Spinner />;
} else if (this.state.progress === "sent_email") {
resetPasswordJsx = (
<div>
<div className="mx_Login_prompt">
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
<br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
@ -174,7 +175,7 @@ module.exports = React.createClass({
);
} else if (this.state.progress === "complete") {
resetPasswordJsx = (
<div>
<div className="mx_Login_prompt">
<p>{ _t('Your password has been reset') }.</p>
<p>{ _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') }.</p>
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
@ -182,6 +183,20 @@ module.exports = React.createClass({
</div>
);
} else {
let serverConfigSection;
if (!config.disable_custom_urls) {
serverConfigSection = (
<ServerConfig ref="serverConfig"
withToggleButton={true}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0} />
);
}
resetPasswordJsx = (
<div>
<div className="mx_Login_prompt">
@ -209,16 +224,7 @@ module.exports = React.createClass({
<br />
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
</form>
<ServerConfig ref="serverConfig"
withToggleButton={true}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0} />
<div className="mx_Login_error">
</div>
{ serverConfigSection }
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{ _t('Return to login screen') }
</a>
@ -233,12 +239,12 @@ module.exports = React.createClass({
return (
<div className="mx_Login">
<LoginPage>
<div className="mx_Login_box">
<LoginHeader />
{ resetPasswordJsx }
</div>
</div>
</LoginPage>
);
},
});

View file

@ -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();
},
@ -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) {
if (SdkConfig.get().disable_custom_urls) {
errorText = (
<div>
<div>{ _t('Incorrect username and/or password.') }</div>
<div className="mx_Login_smallError">
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
{
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
})
}
</div>
</div>
);
} else {
errorText = _t('Incorrect username and/or password.');
}
} else {
// other errors, not specific to doing a password login
errorText = this._errorTextFromError(error);
@ -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 = <span>
{ _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 <a>enable unsafe scripts</a>.",
/<a>(.*?)<\/a>/,
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; },
{},
{ 'a': (sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; } },
) }
</span>;
} else {
errorText = <span>
{ _tJsx("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
/<a>(.*?)<\/a>/,
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; },
{
_t("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
{},
{ 'a': (sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; } },
) }
</span>;
}
@ -276,8 +328,20 @@ module.exports = React.createClass({
},
componentForStep: function(step) {
switch (step) {
case 'm.login.password':
if (!step) {
return null;
}
const stepRenderer = this._stepRendererMap[step];
if (stepRenderer) {
return stepRenderer();
}
return null;
},
_renderPasswordStep: function() {
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
return (
<PasswordLogin
@ -293,26 +357,18 @@ module.exports = React.createClass({
hsUrl={this.state.enteredHomeserverUrl}
/>
);
case 'm.login.cas':
},
_renderCasStep: function() {
const CasLogin = sdk.getComponent('login.CasLogin');
return (
<CasLogin onSubmit={this.onCasLogin} />
);
default:
if (!step) {
return;
}
return (
<div>
{ _t('Sorry, this homeserver is using a login which is not recognised ') }({ step })
</div>
);
}
},
_onLanguageChange: function(newLang) {
if (languageHandler.getCurrentLanguage() !== newLang) {
UserSettingsStore.setLocalSetting('language', newLang);
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
PlatformPeg.get().reload();
}
},
@ -329,6 +385,7 @@ module.exports = React.createClass({
render: function() {
const Loader = sdk.getComponent("elements.Spinner");
const LoginPage = sdk.getComponent("login.LoginPage");
const LoginHeader = sdk.getComponent("login.LoginHeader");
const LoginFooter = sdk.getComponent("login.LoginFooter");
const ServerConfig = sdk.getComponent("login.ServerConfig");
@ -343,43 +400,68 @@ module.exports = React.createClass({
}
let returnToAppJsx;
/*
// with the advent of ILAG I don't think we need this any more
if (this.props.onCancelClick) {
returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
{ _t('Return to app') }
</a>;
}
*/
return (
<div className="mx_Login">
<div className="mx_Login_box">
<LoginHeader />
<div>
<h2>{ _t('Sign in') }
{ loader }
</h2>
{ this.componentForStep(this.state.currentFlow) }
<ServerConfig ref="serverConfig"
let serverConfig;
let header;
if (!SdkConfig.get().disable_custom_urls) {
serverConfig = <ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000} />
delayTimeMs={1000} />;
}
// FIXME: remove status.im theme tweaks
const theme = SettingsStore.getValue("theme");
if (theme !== "status") {
header = <h2>{ _t('Sign in') }</h2>;
} else {
if (!this.state.errorText) {
header = <h2>{ _t('Sign in to get started') }</h2>;
}
}
let errorTextSection;
if (this.state.errorText) {
errorTextSection = (
<div className="mx_Login_error">
{ this.state.errorText }
</div>
);
}
return (
<LoginPage>
<div className="mx_Login_box">
<LoginHeader />
<div>
{ header }
{ errorTextSection }
{ this.componentForStep(this.state.currentFlow) }
{ serverConfig }
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
{ _t('Create an account') }
</a>
{ loginAsGuestJsx }
{ returnToAppJsx }
{ this._renderLanguageSetting() }
{ !SdkConfig.get().disable_login_language_selector ? this._renderLanguageSetting() : '' }
<LoginFooter />
</div>
</div>
</div>
</LoginPage>
);
},
});

View file

@ -59,9 +59,10 @@ module.exports = React.createClass({
render: function() {
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
const LoginPage = sdk.getComponent('login.LoginPage');
const LoginHeader = sdk.getComponent('login.LoginHeader');
return (
<div className="mx_Login">
<LoginPage>
<div className="mx_Login_box">
<LoginHeader />
<div className="mx_Login_profile">
@ -74,7 +75,7 @@ module.exports = React.createClass({
{ this.state.errorString }
</div>
</div>
</div>
</LoginPage>
);
},
});

View file

@ -26,6 +26,8 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import RegistrationForm from '../../views/login/RegistrationForm';
import RtsClient from '../../../RtsClient';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
const MIN_PASSWORD_LENGTH = 6;
@ -302,7 +304,7 @@ module.exports = React.createClass({
} : {};
return this._matrixClient.register(
this.state.formVals.username.toLowerCase(),
this.state.formVals.username,
this.state.formVals.password,
undefined, // session id: included in the auth dict already
auth,
@ -322,10 +324,13 @@ module.exports = React.createClass({
render: function() {
const LoginHeader = sdk.getComponent('login.LoginHeader');
const LoginFooter = sdk.getComponent('login.LoginFooter');
const LoginPage = sdk.getComponent('login.LoginPage');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent("elements.Spinner");
const ServerConfig = sdk.getComponent('views.login.ServerConfig');
const theme = SettingsStore.getValue("theme");
let registerBody;
if (this.state.doingUIAuth) {
registerBody = (
@ -344,9 +349,19 @@ module.exports = React.createClass({
} else if (this.state.busy || this.state.teamServerBusy) {
registerBody = <Spinner />;
} else {
let errorSection;
if (this.state.errorText) {
errorSection = <div className="mx_Login_error">{ this.state.errorText }</div>;
let serverConfigSection;
if (!SdkConfig.get().disable_custom_urls) {
serverConfigSection = (
<ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>
);
}
registerBody = (
<div>
@ -362,21 +377,14 @@ module.exports = React.createClass({
onRegisterClick={this.onFormSubmit}
onTeamSelected={this.onTeamSelected}
/>
{ errorSection }
<ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>
{ serverConfigSection }
</div>
);
}
let returnToAppJsx;
/*
// with the advent of ILAG I don't think we need this any more
if (this.props.onCancelClick) {
returnToAppJsx = (
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
@ -384,8 +392,31 @@ module.exports = React.createClass({
</a>
);
}
*/
let header;
let errorText;
// FIXME: remove hardcoded Status team tweaks at some point
if (theme === 'status' && this.state.errorText) {
header = <div className="mx_Login_error">{ this.state.errorText }</div>;
} else {
header = <h2>{ _t('Create an account') }</h2>;
if (this.state.errorText) {
errorText = <div className="mx_Login_error">{ this.state.errorText }</div>;
}
}
let signIn;
if (!this.state.doingUIAuth) {
signIn = (
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{ theme === 'status' ? _t('Sign in') : _t('I already have an account') }
</a>
);
}
return (
<div className="mx_Login">
<LoginPage>
<div className="mx_Login_box">
<LoginHeader
icon={this.state.teamSelected ?
@ -393,15 +424,14 @@ module.exports = React.createClass({
this.state.teamSelected.domain + "/icon.png" :
null}
/>
<h2>{ _t('Create an account') }</h2>
{ header }
{ registerBody }
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{ _t('I already have an account') }
</a>
{ signIn }
{ errorText }
{ returnToAppJsx }
<LoginFooter />
</div>
</div>
</LoginPage>
);
},
});

View file

@ -0,0 +1,168 @@
/*
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.
*/
'use strict';
import React from "react";
import * as sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton';
import Presence from "../../../Presence";
import dispatcher from "../../../dispatcher";
import * as ContextualMenu from "../../structures/ContextualMenu";
import SettingsStore from "../../../settings/SettingsStore";
// This is an avatar with presence information and controls on it.
module.exports = React.createClass({
displayName: 'MemberPresenceAvatar',
propTypes: {
member: React.PropTypes.object.isRequired,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string,
},
getDefaultProps: function() {
return {
width: 40,
height: 40,
resizeMethod: 'crop',
};
},
getInitialState: function() {
let presenceState = null;
let presenceMessage = null;
// RoomMembers do not necessarily have a user.
if (this.props.member.user) {
presenceState = this.props.member.user.presence;
presenceMessage = this.props.member.user.presenceStatusMsg;
}
return {
status: presenceState,
message: presenceMessage,
};
},
componentWillMount: function() {
MatrixClientPeg.get().on("User.presence", this.onUserPresence);
this.dispatcherRef = dispatcher.register(this.onAction);
},
componentWillUnmount: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("User.presence", this.onUserPresence);
}
dispatcher.unregister(this.dispatcherRef);
},
onAction: function(payload) {
if (payload.action !== "self_presence_updated") return;
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) return;
this.setState({
status: payload.statusInfo.presence,
message: payload.statusInfo.status_msg,
});
},
onUserPresence: function(event, user) {
if (user.userId !== MatrixClientPeg.get().getUserId()) return;
this.setState({
status: user.presence,
message: user.presenceStatusMsg,
});
},
onStatusChange: function(newStatus) {
Presence.stopMaintainingStatus();
if (newStatus === "online") {
Presence.setState(newStatus);
} else Presence.setState(newStatus, null, true);
},
onClick: function(e) {
const PresenceContextMenu = sdk.getComponent('context_menus.PresenceContextMenu');
const elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
const chevronOffset = 12;
let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
ContextualMenu.createMenu(PresenceContextMenu, {
chevronOffset: chevronOffset,
chevronFace: 'bottom',
left: x,
top: y,
menuWidth: 125,
currentStatus: this.state.status,
onChange: this.onStatusChange,
});
e.stopPropagation();
// XXX NB the following assumes that user is non-null, which is not valid
// const presenceState = this.props.member.user.presence;
// const presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
// const presenceLastTs = this.props.member.user.lastPresenceTs;
// const presenceCurrentlyActive = this.props.member.user.currentlyActive;
// const presenceMessage = this.props.member.user.presenceStatusMsg;
},
render: function() {
const MemberAvatar = sdk.getComponent("avatars.MemberAvatar");
let onClickFn = null;
if (this.props.member.userId === MatrixClientPeg.get().getUserId()) {
onClickFn = this.onClick;
}
const avatarNode = (
<MemberAvatar member={this.props.member} width={this.props.width} height={this.props.height}
resizeMethod={this.props.resizeMethod} />
);
let statusNode = (
<span className={"mx_MemberPresenceAvatar_status mx_MemberPresenceAvatar_status_" + this.state.status} />
);
// LABS: Disable presence management functions for now
// Also disable the presence information if there's no status information
if (!SettingsStore.isFeatureEnabled("feature_presence_management") || !this.state.status) {
statusNode = null;
onClickFn = null;
}
let avatar = (
<div className="mx_MemberPresenceAvatar">
{ avatarNode }
{ statusNode }
</div>
);
if (onClickFn) {
avatar = (
<AccessibleButton onClick={onClickFn} className="mx_MemberPresenceAvatar" element="div">
{ avatarNode }
{ statusNode }
</AccessibleButton>
);
}
return avatar;
},
});

View file

@ -34,6 +34,8 @@ module.exports = React.createClass({
propTypes: {
title: PropTypes.string.isRequired,
description: PropTypes.node,
// Extra node inserted after picker input, dropdown and errors
extraNode: PropTypes.node,
value: PropTypes.string,
placeholder: PropTypes.string,
roomId: PropTypes.string,
@ -242,7 +244,7 @@ module.exports = React.createClass({
_doNaiveGroupRoomSearch: function(query) {
const lowerCaseQuery = query.toLowerCase();
const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), this.props.groupId);
const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
const results = [];
groupStore.getGroupRooms().forEach((r) => {
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
@ -268,34 +270,53 @@ module.exports = React.createClass({
const rooms = MatrixClientPeg.get().getRooms();
const results = [];
rooms.forEach((room) => {
let rank = Infinity;
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
const name = nameEvent ? nameEvent.getContent().name : '';
const canonicalAlias = room.getCanonicalAlias();
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
return a.concat(b);
}, []);
const topic = topicEvent ? topicEvent.getContent().topic : '';
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = aliases.some((alias) =>
(alias || '').toLowerCase().includes(lowerCaseQuery),
);
const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery);
if (!(nameMatch || topicMatch || aliasMatch)) {
let aliasMatch = false;
let shortestMatchingAliasLength = Infinity;
aliases.forEach((alias) => {
if ((alias || '').toLowerCase().includes(lowerCaseQuery)) {
aliasMatch = true;
if (shortestMatchingAliasLength > alias.length) {
shortestMatchingAliasLength = alias.length;
}
}
});
if (!(nameMatch || aliasMatch)) {
return;
}
if (aliasMatch) {
// A shorter matching alias will give a better rank
rank = shortestMatchingAliasLength;
}
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
results.push({
rank,
room_id: room.roomId,
avatar_url: avatarUrl,
name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
});
});
this._processResults(results, query);
// Sort by rank ascending (a high rank being less relevant)
const sortedResults = results.sort((a, b) => {
return a.rank - b.rank;
});
this._processResults(sortedResults, query);
this.setState({
busy: false,
});
@ -574,6 +595,7 @@ module.exports = React.createClass({
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
{ error }
{ addressSelector }
{ this.props.extraNode }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import * as KeyCode from '../../../KeyCode';
import { KeyCode } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index';

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import classnames from 'classnames';
import { _t } from '../../../languageHandler';
/*
@ -24,51 +23,17 @@ import { _t } from '../../../languageHandler';
*/
export default React.createClass({
displayName: 'ConfirmRedactDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
},
defaultProps: {
danger: false,
},
onOk: function() {
this.props.onFinished(true);
},
onCancel: function() {
this.props.onFinished(false);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const title = _t("Confirm Removal");
const confirmButtonClass = classnames({
'mx_Dialog_primary': true,
'danger': false,
});
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
return (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={title}
>
<div className="mx_Dialog_content">
{ _t("Are you sure you wish to remove (delete) this event? " +
<QuestionDialog onFinished={this.props.onFinished}
title={_t("Confirm Removal")}
description={
_t("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.")}
</div>
<div className="mx_Dialog_buttons">
<button className={confirmButtonClass} onClick={this.onOk}>
{ _t("Remove") }
</button>
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
</div>
</BaseDialog>
button={_t("Remove")}>
</QuestionDialog>
);
},
});

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames';
@ -35,6 +36,8 @@ export default React.createClass({
member: React.PropTypes.object,
// group member object. Supply either this or 'member'
groupMember: GroupMemberType,
// needed if a group member is specified
matrixClient: React.PropTypes.instanceOf(MatrixClient),
action: React.PropTypes.string.isRequired, // eg. 'Ban'
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
@ -104,10 +107,11 @@ export default React.createClass({
name = this.props.member.name;
userId = this.props.member.userId;
} else {
// we don't get this info from the API yet
avatar = <BaseAvatar name={this.props.groupMember.userId} width={48} height={48} />;
name = this.props.groupMember.userId;
const httpAvatarUrl = this.props.groupMember.avatarUrl ?
this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null;
name = this.props.groupMember.displayname || this.props.groupMember.userId;
userId = this.props.groupMember.userId;
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
}
return (

View file

@ -18,7 +18,7 @@ import React from 'react';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t, _tJsx } from '../../../languageHandler';
import { _t } from '../../../languageHandler';
export default React.createClass({
@ -45,9 +45,10 @@ export default React.createClass({
if (SdkConfig.get().bug_report_endpoint_url) {
bugreport = (
<p>
{ _tJsx(
{ _t(
"Otherwise, <a>click here</a> to send a bug report.",
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a>,
{},
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
) }
</p>
);

View file

@ -20,8 +20,8 @@ import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
import KeyCode from '../../../KeyCode';
import { _t, _tJsx } from '../../../languageHandler';
import { KeyCode } from '../../../Keyboard';
import { _t } from '../../../languageHandler';
// The amount of time to wait for further changes to the input username before
// sending a request to the server
@ -267,24 +267,21 @@ export default React.createClass({
</div>
{ usernameIndicator }
<p>
{ _tJsx(
{ _t(
'This will be your account name on the <span></span> ' +
'homeserver, or you can pick a <a>different server</a>.',
[
/<span><\/span>/,
/<a>(.*?)<\/a>/,
],
[
(sub) => <span>{ this.props.homeserverUrl }</span>,
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
],
{},
{
'span': <span>{ this.props.homeserverUrl }</span>,
'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
},
) }
</p>
<p>
{ _tJsx(
{ _t(
'If you already have a Matrix account you can <a>log in</a> instead.',
/<a>(.*?)<\/a>/,
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a>],
{},
{ 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
) }
</p>
{ auth }

View file

@ -20,6 +20,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
function DeviceListEntry(props) {
const {userId, device} = props;
@ -49,8 +50,7 @@ function UserUnknownDeviceList(props) {
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
<DeviceListEntry key={deviceId} userId={userId}
device={userDevices[deviceId]}
/>,
device={userDevices[deviceId]} />,
);
return (
@ -93,66 +93,28 @@ export default React.createClass({
propTypes: {
room: React.PropTypes.object.isRequired,
// map from userid -> deviceid -> deviceinfo
devices: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired,
},
componentWillMount: function() {
this._unmounted = false;
const roomMembers = this.props.room.getJoinedMembers().map((m) => {
return m.userId;
});
this.setState({
// map from userid -> deviceid -> deviceinfo
devices: null,
});
MatrixClientPeg.get().downloadKeys(roomMembers, false).then((devices) => {
if (this._unmounted) return;
const unknownDevices = {};
// This is all devices in this room, so find the unknown ones.
Object.keys(devices).forEach((userId) => {
Object.keys(devices[userId]).map((deviceId) => {
const device = devices[userId][deviceId];
if (device.isUnverified() && !device.isKnown()) {
if (unknownDevices[userId] === undefined) {
unknownDevices[userId] = {};
}
unknownDevices[userId][deviceId] = device;
}
componentDidMount: function() {
// Given we've now shown the user the unknown device, it is no longer
// unknown to them. Therefore mark it as 'known'.
if (!device.isKnown()) {
Object.keys(this.props.devices).forEach((userId) => {
Object.keys(this.props.devices[userId]).map((deviceId) => {
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
}
});
});
this.setState({
devices: unknownDevices,
});
});
},
componentWillUnmount: function() {
this._unmounted = true;
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('Opening UnknownDeviceDialog');
},
render: function() {
if (this.state.devices === null) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
}
const client = MatrixClientPeg.get();
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
this.props.room.getBlacklistUnverifiedDevices();
let warning;
if (blacklistUnverified) {
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
warning = (
<h4>
{ _t("You are currently blacklisting unverified devices; to send " +
@ -189,7 +151,7 @@ export default React.createClass({
{ warning }
{ _t("Unknown devices") }:
<UnknownDeviceList devices={this.state.devices} />
<UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" autoFocus={true}

View file

@ -77,6 +77,7 @@ export default React.createClass({
onClick={this._onClick}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}
aria-label={this.props.label}
>
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
{ tooltip }

View file

@ -17,10 +17,13 @@ limitations under the License.
'use strict';
import url from 'url';
import qs from 'querystring';
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import WidgetMessaging from '../../../WidgetMessaging';
import TintableSvgButton from './TintableSvgButton';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t, _td } from '../../../languageHandler';
@ -51,42 +54,92 @@ export default React.createClass({
creatorUserId: React.PropTypes.string,
},
getDefaultProps: function() {
getDefaultProps() {
return {
url: "",
};
},
getInitialState: function() {
const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_');
/**
* Set initial component state when the App wUrl (widget URL) is being updated.
* Component props *must* be passed (rather than relying on this.props).
* @param {Object} newProps The new properties of the component
* @return {Object} Updated component state to be set with setState
*/
_getNewState(newProps) {
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
return {
loading: false,
widgetUrl: this.props.url,
initialising: true, // True while we are mangling the widget URL
loading: true, // True while the iframe content is loading
widgetUrl: this._addWurlParams(newProps.url),
widgetPermissionId: widgetPermissionId,
// Assume that widget has permission to load if we are the user who added it to the room, or if explicitly granted by the user
hasPermissionToLoad: hasPermissionToLoad === 'true' || this.props.userId === this.props.creatorUserId,
// Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
error: null,
deleting: false,
widgetPageTitle: null,
};
},
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
isScalarUrl: function() {
/**
* Add widget instance specific parameters to pass in wUrl
* Properties passed to widget instance:
* - widgetId
* - origin / parent URL
* @param {string} urlString Url string to modify
* @return {string}
* Url string with parameters appended.
* If url can not be parsed, it is returned unmodified.
*/
_addWurlParams(urlString) {
const u = url.parse(urlString);
if (!u) {
console.error("_addWurlParams", "Invalid URL", urlString);
return url;
}
const params = qs.parse(u.query);
// Append widget ID to query parameters
params.widgetId = this.props.id;
// Append current / parent URL
params.parentUrl = window.location.href;
u.search = undefined;
u.query = params;
return u.format();
},
getInitialState() {
return this._getNewState(this.props);
},
/**
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
* @param {[type]} url URL to check
* @return {Boolean} True if specified URL is a scalar URL
*/
isScalarUrl(url) {
if (!url) {
console.error('Scalar URL check failed. No URL specified');
return false;
}
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
if (!scalarUrls || scalarUrls.length == 0) {
scalarUrls = [SdkConfig.get().integrations_rest_url];
}
for (let i = 0; i < scalarUrls.length; i++) {
if (this.props.url.startsWith(scalarUrls[i])) {
if (url.startsWith(scalarUrls[i])) {
return true;
}
}
return false;
},
isMixedContent: function() {
isMixedContent() {
const parentContentProtocol = window.location.protocol;
const u = url.parse(this.props.url);
const childContentProtocol = u.protocol;
@ -98,43 +151,77 @@ export default React.createClass({
return false;
},
componentWillMount: function() {
if (!this.isScalarUrl()) {
componentWillMount() {
WidgetMessaging.startListening();
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
window.addEventListener('message', this._onMessage, false);
this.setScalarToken();
},
/**
* Adds a scalar token to the widget URL, if required
* Component initialisation is only complete when this function has resolved
*/
setScalarToken() {
this.setState({initialising: true});
if (!this.isScalarUrl(this.props.url)) {
console.warn('Non-scalar widget, not setting scalar token!', url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
initialising: false,
});
return;
}
// Fetch the token before loading the iframe as we need to mangle the URL
this.setState({
loading: true,
});
// Fetch the token before loading the iframe as we need it to mangle the URL
if (!this._scalarClient) {
this._scalarClient = new ScalarAuthClient();
}
this._scalarClient.getScalarToken().done((token) => {
// Append scalar_token as a query param
// Append scalar_token as a query param if not already present
this._scalarClient.scalarToken = token;
const u = url.parse(this.props.url);
if (!u.search) {
u.search = "?scalar_token=" + encodeURIComponent(token);
} else {
u.search += "&scalar_token=" + encodeURIComponent(token);
const u = url.parse(this._addWurlParams(this.props.url));
const params = qs.parse(u.query);
if (!params.scalar_token) {
params.scalar_token = encodeURIComponent(token);
// u.search must be set to undefined, so that u.format() uses query paramerters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
u.search = undefined;
u.query = params;
}
this.setState({
error: null,
widgetUrl: u.format(),
loading: false,
initialising: false,
});
}, (err) => {
console.error("Failed to get scalar_token", err);
this.setState({
error: err.message,
loading: false,
initialising: false,
});
});
window.addEventListener('message', this._onMessage, false);
},
componentWillUnmount() {
WidgetMessaging.stopListening();
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
window.removeEventListener('message', this._onMessage);
},
componentWillReceiveProps(nextProps) {
if (nextProps.url !== this.props.url) {
this._getNewState(nextProps);
this.setScalarToken();
} else if (nextProps.show && !this.props.show) {
this.setState({
loading: true,
});
}
},
_onMessage(event) {
if (this.props.type !== 'jitsi') {
return;
@ -154,11 +241,11 @@ export default React.createClass({
}
},
_canUserModify: function() {
_canUserModify() {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
},
_onEditClick: function(e) {
_onEditClick(e) {
console.log("Edit widget ID ", this.props.id);
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
@ -168,9 +255,10 @@ export default React.createClass({
}, "mx_IntegrationsManager");
},
/* If user has permission to modify widgets, delete the widget, otherwise revoke access for the widget to load in the user's browser
/* If user has permission to modify widgets, delete the widget,
* otherwise revoke access for the widget to load in the user's browser
*/
_onDeleteClick: function() {
_onDeleteClick() {
if (this._canUserModify()) {
// Show delete confirmation dialog
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@ -202,6 +290,23 @@ export default React.createClass({
}
},
/**
* Called when widget iframe has finished loading
*/
_onLoaded() {
this.setState({loading: false});
},
/**
* Set remote content title on AppTile
* @param {string} title Title string to set on the AppTile
*/
_updateWidgetTitle(title) {
if (title) {
this.setState({widgetPageTitle: null});
}
},
// Widget labels to render, depending upon user permissions
// These strings are translated at the point that they are inserted in to the DOM, in the render method
_deleteWidgetLabel() {
@ -224,7 +329,7 @@ export default React.createClass({
this.setState({hasPermissionToLoad: false});
},
formatAppTileName: function() {
formatAppTileName() {
let appTileName = "No name";
if (this.props.name && this.props.name.trim()) {
appTileName = this.props.name.trim();
@ -232,7 +337,7 @@ export default React.createClass({
return appTileName;
},
onClickMenuBar: function(ev) {
onClickMenuBar(ev) {
ev.preventDefault();
// Ignore clicks on menu bar children
@ -247,7 +352,16 @@ export default React.createClass({
});
},
render: function() {
_getSafeUrl() {
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
return safeWidgetUrl;
},
render() {
let appTileBody;
// Don't render widget if it is in the process of being deleted
@ -262,36 +376,32 @@ export default React.createClass({
// a link to it.
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
"allow-same-origin allow-scripts allow-presentation";
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
if (this.props.show) {
if (this.state.loading) {
appTileBody = (
const loadingElement = (
<div className='mx_AppTileBody mx_AppLoading'>
<MessageSpinner msg='Loading...' />
</div>
);
if (this.state.initialising) {
appTileBody = loadingElement;
} else if (this.state.hasPermissionToLoad == true) {
if (this.isMixedContent()) {
appTileBody = (
<div className="mx_AppTileBody">
<AppWarning
errorMsg="Error - Mixed content"
/>
<AppWarning errorMsg="Error - Mixed content" />
</div>
);
} else {
appTileBody = (
<div className="mx_AppTileBody">
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
{ this.state.loading && loadingElement }
<iframe
ref="appFrame"
src={safeWidgetUrl}
src={this._getSafeUrl()}
allowFullScreen="true"
sandbox={sandboxFlags}
onLoad={this._onLoaded}
></iframe>
</div>
);
@ -313,8 +423,8 @@ export default React.createClass({
// editing is done in scalar
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
const deleteWidgetLabel = this._deleteWidgetLabel();
let deleteIcon = 'img/cancel.svg';
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
let deleteIcon = 'img/cancel_green.svg';
let deleteClasses = 'mx_AppTileMenuBarWidget';
if (this._canUserModify()) {
deleteIcon = 'img/icon-delete-pink.svg';
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
@ -323,25 +433,29 @@ export default React.createClass({
return (
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
{ this.formatAppTileName() }
<b>{ this.formatAppTileName() }</b>
{ this.state.widgetPageTitle && (
<span>&nbsp;-&nbsp;{ this.state.widgetPageTitle }</span>
) }
<span className="mx_AppTileMenuBarWidgets">
{ /* Edit widget */ }
{ showEditButton && <img
src="img/edit.svg"
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
width="8" height="8"
alt={_t('Edit')}
{ showEditButton && <TintableSvgButton
src="img/edit_green.svg"
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Edit')}
onClick={this._onEditClick}
width="10"
height="10"
/> }
{ /* Delete widget */ }
<img src={deleteIcon}
<TintableSvgButton
src={deleteIcon}
className={deleteClasses}
width="8" height="8"
alt={_t(deleteWidgetLabel)}
title={_t(deleteWidgetLabel)}
onClick={this._onDeleteClick}
width="10"
height="10"
/>
</span>
</div>

View file

@ -19,7 +19,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import UserSettingsStore from '../../../UserSettingsStore';
import FlairStore from '../../../stores/FlairStore';
import dis from '../../../dispatcher';
@ -43,18 +42,22 @@ class FlairAvatar extends React.Component {
render() {
const httpUrl = this.context.matrixClient.mxcUrlToHttp(
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
const tooltip = this.props.groupProfile.name ?
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
this.props.groupProfile.groupId;
return <img
src={httpUrl}
width="16"
height="16"
onClick={this.onClick}
title={this.props.groupProfile.groupId} />;
title={tooltip} />;
}
}
FlairAvatar.propTypes = {
groupProfile: PropTypes.shape({
groupId: PropTypes.string.isRequired,
name: PropTypes.string,
avatarUrl: PropTypes.string.isRequired,
}),
};
@ -69,26 +72,19 @@ export default class Flair extends React.Component {
this.state = {
profiles: [],
};
this.onRoomStateEvents = this.onRoomStateEvents.bind(this);
}
componentWillUnmount() {
this._unmounted = true;
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
}
componentWillMount() {
this._unmounted = false;
if (UserSettingsStore.isFeatureEnabled('feature_groups') && FlairStore.groupSupport()) {
this._generateAvatars();
}
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
this._generateAvatars(this.props.groups);
}
onRoomStateEvents(event) {
if (event.getType() === 'm.room.related_groups' && FlairStore.groupSupport()) {
this._generateAvatars();
}
componentWillReceiveProps(newProps) {
this._generateAvatars(newProps.groups);
}
async _getGroupProfiles(groups) {
@ -105,23 +101,7 @@ export default class Flair extends React.Component {
return profiles.filter((p) => p !== null);
}
async _generateAvatars() {
let groups = await FlairStore.getPublicisedGroupsCached(this.context.matrixClient, this.props.userId);
if (this.props.roomId && this.props.showRelated) {
const relatedGroupsEvent = this.context.matrixClient
.getRoom(this.props.roomId)
.currentState
.getStateEvents('m.room.related_groups', '');
const relatedGroups = relatedGroupsEvent ?
relatedGroupsEvent.getContent().groups || [] : [];
if (relatedGroups && relatedGroups.length > 0) {
groups = groups.filter((groupId) => {
return relatedGroups.includes(groupId);
});
} else {
groups = [];
}
}
async _generateAvatars(groups) {
if (!groups || groups.length === 0) {
return;
}
@ -147,13 +127,7 @@ export default class Flair extends React.Component {
}
Flair.propTypes = {
userId: PropTypes.string,
// Whether to show only the flair associated with related groups of the given room,
// or all flair associated with a user.
showRelated: PropTypes.bool,
// The room that this flair will be displayed in. Optional. Only applies when showRelated = true.
roomId: PropTypes.string,
groups: PropTypes.arrayOf(PropTypes.string),
};
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';

View file

@ -18,8 +18,8 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import UserSettingsStore from '../../../UserSettingsStore';
import * as languageHandler from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
@ -54,9 +54,9 @@ export default class LanguageDropdown extends React.Component {
// If no value is given, we start with the first
// country selected, but our parent component
// doesn't know this, therefore we do this.
const _localSettings = UserSettingsStore.getLocalSettings();
if (_localSettings.hasOwnProperty('language')) {
this.props.onOptionChange(_localSettings.language);
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
if (language) {
this.props.onOptionChange(language);
} else {
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
this.props.onOptionChange(language);
@ -95,12 +95,12 @@ export default class LanguageDropdown extends React.Component {
// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propgating
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
let value = null;
const _localSettings = UserSettingsStore.getLocalSettings();
if (_localSettings.hasOwnProperty('language')) {
value = this.props.value || _localSettings.language;
if (language) {
value = this.props.value || language;
} else {
const language = navigator.language || navigator.userLanguage;
language = navigator.language || navigator.userLanguage;
value = this.props.value || language;
}

View file

@ -304,7 +304,7 @@ module.exports = React.createClass({
return items[0];
} else if (remaining > 0) {
items = items.slice(0, itemLimit);
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } )
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
} else {
const lastItem = items.pop();
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });

View file

@ -20,14 +20,16 @@ import React from 'react';
import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
let LEVEL_ROLE_MAP = {};
const reverseRoles = {};
module.exports = React.createClass({
displayName: 'PowerSelector',
propTypes: {
value: React.PropTypes.number.isRequired,
// The maximum value that can be set with the power selector
maxValue: React.PropTypes.number.isRequired,
// Default user power level for the room
usersDefault: React.PropTypes.number.isRequired,
// if true, the <select/> should be a 'controlled' form element and updated by React
// to reflect the current value, rather than left freeform.
@ -43,78 +45,98 @@ module.exports = React.createClass({
getInitialState: function() {
return {
custom: (LEVEL_ROLE_MAP[this.props.value] === undefined),
levelRoleMap: {},
// List of power levels to show in the drop-down
options: [],
};
},
getDefaultProps: function() {
return {
maxValue: Infinity,
usersDefault: 0,
};
},
componentWillMount: function() {
LEVEL_ROLE_MAP = Roles.levelRoleMap();
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) {
reverseRoles[LEVEL_ROLE_MAP[key]] = key;
this._initStateFromProps(this.props);
},
componentWillReceiveProps: function(newProps) {
this._initStateFromProps(newProps);
},
_initStateFromProps: function(newProps) {
// This needs to be done now because levelRoleMap has translated strings
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
const options = Object.keys(levelRoleMap).filter((l) => {
return l === undefined || l <= newProps.maxValue;
});
this.setState({
levelRoleMap,
options,
custom: levelRoleMap[newProps.value] === undefined,
});
},
onSelectChange: function(event) {
this.setState({ custom: event.target.value === "Custom" });
if (event.target.value !== "Custom") {
this.props.onChange(this.getValue());
this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
if (event.target.value !== "SELECT_VALUE_CUSTOM") {
this.props.onChange(event.target.value);
}
},
onCustomBlur: function(event) {
this.props.onChange(this.getValue());
this.props.onChange(parseInt(this.refs.custom.value));
},
onCustomKeyDown: function(event) {
if (event.key == "Enter") {
this.props.onChange(this.getValue());
this.props.onChange(parseInt(this.refs.custom.value));
}
},
getValue: function() {
let value;
if (this.refs.select) {
value = reverseRoles[this.refs.select.value];
if (this.refs.custom) {
if (value === undefined) value = parseInt( this.refs.custom.value );
}
}
return value;
},
render: function() {
let customPicker;
if (this.state.custom) {
let input;
if (this.props.disabled) {
input = <span>{ this.props.value }</span>;
customPicker = <span>{ _t(
"Custom of %(powerLevel)s",
{ powerLevel: this.props.value },
) }</span>;
} else {
input = <input ref="custom" type="text" size="3" defaultValue={this.props.value} onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} />;
customPicker = <span> = <input
ref="custom"
type="text"
size="3"
defaultValue={this.props.value}
onBlur={this.onCustomBlur}
onKeyDown={this.onCustomKeyDown}
/>
</span>;
}
customPicker = <span> of { input }</span>;
}
let selectValue;
if (this.state.custom) {
selectValue = "Custom";
selectValue = "SELECT_VALUE_CUSTOM";
} else {
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom";
selectValue = this.state.levelRoleMap[this.props.value] ?
this.props.value : "SELECT_VALUE_CUSTOM";
}
let select;
if (this.props.disabled) {
select = <span>{ selectValue }</span>;
select = <span>{ this.state.levelRoleMap[selectValue] }</span>;
} else {
// Each level must have a definition in LEVEL_ROLE_MAP
const levels = [0, 50, 100];
let options = levels.map((level) => {
// Each level must have a definition in this.state.levelRoleMap
let options = this.state.options.map((level) => {
return {
value: LEVEL_ROLE_MAP[level],
// Give a userDefault (users_default in the power event) of 0 but
// because level !== undefined, this should never be used.
text: Roles.textualPowerLevel(level, 0),
value: level,
text: Roles.textualPowerLevel(level, this.props.usersDefault),
};
});
options.push({ value: "Custom", text: _t("Custom level") });
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
options = options.map((op) => {
return <option value={op.value} key={op.value}>{ op.text }</option>;
});

View file

@ -0,0 +1,110 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'SettingsFlag',
propTypes: {
name: React.PropTypes.string.isRequired,
level: React.PropTypes.string.isRequired,
roomId: React.PropTypes.string, // for per-room settings
label: React.PropTypes.string, // untranslated
onChange: React.PropTypes.func,
isExplicit: React.PropTypes.bool,
manualSave: React.PropTypes.bool,
// If group is supplied, then this will create a radio button instead.
group: React.PropTypes.string,
value: React.PropTypes.any, // the value for the radio button
},
getInitialState: function() {
return {
value: SettingsStore.getValueAt(
this.props.level,
this.props.name,
this.props.roomId,
this.props.isExplicit,
),
};
},
onChange: function(e) {
if (this.props.group && !e.target.checked) return;
const newState = this.props.group ? this.props.value : e.target.checked;
if (!this.props.manualSave) this.save(newState);
else this.setState({ value: newState });
if (this.props.onChange) this.props.onChange(newState);
},
save: function(val = undefined) {
return SettingsStore.setValue(
this.props.name,
this.props.roomId,
this.props.level,
val !== undefined ? val : this.state.value,
);
},
render: function() {
const value = this.props.manualSave ? this.state.value : SettingsStore.getValueAt(
this.props.level,
this.props.name,
this.props.roomId,
this.props.isExplicit,
);
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
let label = this.props.label;
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
else label = _t(label);
// We generate a relatively complex ID to avoid conflicts
const id = this.props.name + "_" + this.props.group + "_" + this.props.value + "_" + this.props.level;
let checkbox = (
<input id={id}
type="checkbox"
defaultChecked={value}
onChange={this.onChange}
disabled={!canChange}
/>
);
if (this.props.group) {
checkbox = (
<input id={id}
type="radio"
name={this.props.group}
value={this.props.value}
checked={value === this.props.value}
onChange={this.onChange}
disabled={!canChange}
/>
);
}
return (
<label>
{ checkbox }
{ label }
</label>
);
},
});

View file

@ -0,0 +1,61 @@
/*
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 TintableSvg from './TintableSvg';
export default class TintableSvgButton extends React.Component {
constructor(props) {
super(props);
}
render() {
let classes = "mx_TintableSvgButton";
if (this.props.className) {
classes += " " + this.props.className;
}
return (
<span
width={this.props.width}
height={this.props.height}
className={classes}>
<TintableSvg
src={this.props.src}
width={this.props.width}
height={this.props.height}
></TintableSvg>
<span
title={this.props.title}
onClick={this.props.onClick} />
</span>
);
}
}
TintableSvgButton.propTypes = {
src: PropTypes.string,
title: PropTypes.string,
className: PropTypes.string,
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
TintableSvgButton.defaultProps = {
onClick: function() {},
};

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
@ -27,6 +28,10 @@ export default React.createClass({
group: PropTypes.object.isRequired,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
onClick: function(e) {
dis.dispatch({
action: 'view_group',
@ -39,13 +44,15 @@ export default React.createClass({
const EmojiText = sdk.getComponent('elements.EmojiText');
const groupName = this.props.group.name || this.props.group.groupId;
const httpAvatarUrl = this.props.group.avatarUrl ?
this.context.matrixClient.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
const av = <BaseAvatar name={groupName} width={24} height={24} url={this.props.group.avatarUrl} />;
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
const label = <EmojiText
element="div"
title={this.props.group.groupId}
className="mx_RoomTile_name"
className="mx_RoomTile_name mx_RoomTile_badgeShown"
dir="auto"
>
{ groupName }

View file

@ -59,9 +59,7 @@ module.exports = React.createClass({
},
_initGroupStore(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(
this.context.matrixClient, this.props.groupId,
);
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
this._groupStore.registerListener(this.onGroupStoreUpdated);
},
@ -83,6 +81,7 @@ module.exports = React.createClass({
_onKick: function() {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, {
matrixClient: this.context.matrixClient,
groupMember: this.props.groupMember,
action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'),
title: this.state.isUserInvited ? _t('Disinvite this user from community?')

View file

@ -20,17 +20,12 @@ import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types';
import withMatrixClient from '../../../wrappers/withMatrixClient';
const INITIAL_LOAD_NUM_MEMBERS = 30;
export default withMatrixClient(React.createClass({
export default React.createClass({
displayName: 'GroupMemberList',
contextTypes: {
matrixClient: PropTypes.object.isRequired,
},
propTypes: {
groupId: PropTypes.string.isRequired,
},
@ -49,7 +44,7 @@ export default withMatrixClient(React.createClass({
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
this._fetchMembers();
});
@ -92,7 +87,7 @@ export default withMatrixClient(React.createClass({
query = (query || "").toLowerCase();
if (query) {
memberList = memberList.filter((m) => {
const matchesName = m.displayname.toLowerCase().indexOf(query) !== -1;
const matchesName = (m.displayname || "").toLowerCase().includes(query);
const matchesId = m.userId.toLowerCase().includes(query);
if (!matchesName && !matchesId) {
@ -108,15 +103,21 @@ export default withMatrixClient(React.createClass({
if (!uniqueMembers[m.userId]) uniqueMembers[m.userId] = m;
});
memberList = Object.keys(uniqueMembers).map((userId) => uniqueMembers[userId]);
// Descending sort on isPrivileged = true = 1 to isPrivileged = false = 0
memberList.sort((a, b) => {
// TODO: should put admins at the top: we don't yet have that info
if (a < b) {
if (a.isPrivileged === b.isPrivileged) {
const aName = a.displayname || a.userId;
const bName = b.displayname || b.userId;
if (aName < bName) {
return -1;
} else if (a > b) {
} else if (aName > bName) {
return 1;
} else {
return 0;
}
} else {
return a.isPrivileged ? -1 : 1;
}
});
const memberTiles = memberList.map((m) => {
@ -168,4 +169,4 @@ export default withMatrixClient(React.createClass({
</div>
);
},
}));
});

View file

@ -0,0 +1,86 @@
/*
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 sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
import { _t } from '../../../languageHandler.js';
export default React.createClass({
displayName: 'GroupPublicityToggle',
propTypes: {
groupId: PropTypes.string.isRequired,
},
getInitialState() {
return {
busy: false,
ready: false,
isGroupPublicised: null,
};
},
componentWillMount: function() {
this._initGroupStore(this.props.groupId);
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
this.setState({
isGroupPublicised: this._groupStore.getGroupPublicity(),
ready: this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
});
});
},
_onPublicityToggle: function(e) {
e.stopPropagation();
this.setState({
busy: true,
// Optimistic early update
isGroupPublicised: !this.state.isGroupPublicised,
});
this._groupStore.setGroupPublicity(!this.state.isGroupPublicised).then(() => {
this.setState({
busy: false,
});
});
},
render() {
const GroupTile = sdk.getComponent('groups.GroupTile');
const input = <input type="checkbox"
onClick={this._onPublicityToggle}
checked={this.state.isGroupPublicised}
/>;
const labelText = !this.state.ready ? _t("Loading...") :
(this.state.isGroupPublicised ?
_t("Flair will appear if enabled in room settings") :
_t("Flair will not appear")
);
return <div className="mx_GroupPublicity_toggle">
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
<label onClick={this._onPublicityToggle}>
{ input }
{ labelText }
</label>
</div>;
},
});

View file

@ -61,9 +61,7 @@ module.exports = React.createClass({
},
_initGroupStore(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(
this.context.matrixClient, this.props.groupId,
);
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
this._groupStore.registerListener(this.onGroupStoreUpdated);
},
@ -137,7 +135,7 @@ module.exports = React.createClass({
const groupId = this.props.groupId;
const roomId = this.props.groupRoomId;
const roomName = this.state.groupRoom.displayname;
this._groupStore.updateGroupRoomAssociation(roomId, isPublic).catch((err) => {
this._groupStore.updateGroupRoomVisibility(roomId, isPublic).catch((err) => {
console.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {

View file

@ -19,15 +19,10 @@ import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
const INITIAL_LOAD_NUM_ROOMS = 30;
export default React.createClass({
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
},
propTypes: {
groupId: PropTypes.string.isRequired,
},
@ -46,7 +41,7 @@ export default React.createClass({
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
this._fetchRooms();
});
@ -94,7 +89,7 @@ export default React.createClass({
let roomList = this.state.rooms;
if (query) {
roomList = roomList.filter((room) => {
const matchesName = (room.name || "").toLowerCase().include(query);
const matchesName = (room.name || "").toLowerCase().includes(query);
const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
return matchesName || matchesAlias;
});

View file

@ -0,0 +1,93 @@
/*
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 React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import FlairStore from '../../../stores/FlairStore';
const GroupTile = React.createClass({
displayName: 'GroupTile',
propTypes: {
groupId: PropTypes.string.isRequired,
// Whether to show the short description of the group on the tile
showDescription: PropTypes.bool,
// Height of the group avatar in pixels
avatarHeight: PropTypes.number,
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
return {
profile: null,
};
},
getDefaultProps() {
return {
showDescription: true,
avatarHeight: 50,
};
},
componentWillMount: function() {
FlairStore.getGroupProfileCached(this.context.matrixClient, this.props.groupId).then((profile) => {
this.setState({profile});
}).catch((err) => {
console.error('Error whilst getting cached profile for GroupTile', err);
});
},
onClick: function(e) {
e.preventDefault();
dis.dispatch({
action: 'view_group',
group_id: this.props.groupId,
});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const profile = this.state.profile || {};
const name = profile.name || this.props.groupId;
const avatarHeight = this.props.avatarHeight;
const descElement = this.props.showDescription ?
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
<div />;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
<div className="mx_GroupTile_avatar">
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
</div>
<div className="mx_GroupTile_profile">
<div className="mx_GroupTile_name">{ name }</div>
{ descElement }
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
</div>
</AccessibleButton>;
},
});
export default GroupTile;

View file

@ -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 = <div />;
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 <GroupPublicityToggle key={index} groupId={groupId} />;
});
text = _t('Display your community flair in rooms configured to show it.');
scrollbox = <div className="mx_GroupUserSettings_groupPublicity_scrollbox">
<GeminiScrollbar>
{ groupPublicityToggles }
</GeminiScrollbar>
</div>;
} else {
text = _t("You're not currently a member of any communities.");
}
return <div>
<h3>{ _t('Flair') }</h3>
<div className="mx_UserSettings_section">
<p>
{ text }
</p>
{ scrollbox }
</div>
</div>;
},
render() {
const groupPublicity = this._renderGroupPublicity();
return <div>
{ groupPublicity }
</div>;
},
});

View file

@ -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 <a>web browser</a>",
/<a>(.*?)<\/a>/,
(sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning);
{},
{ 'a': (sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }}), warning);
this.refs.recaptchaContainer.appendChild(warning);
} else {
const scriptTag = document.createElement('script');

View file

@ -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 (
<div>
<p>{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => <i>{this.props.inputs.emailAddress}</i>) }</p>
<p>{ _t("An email has been sent to %(emailAddress)s",
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> },
) }
</p>
<p>{ _t("Please check your email to continue registration.") }</p>
</div>
);
@ -370,7 +373,10 @@ export const MsisdnAuthEntry = React.createClass({
});
return (
<div>
<p>{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => <i>{this._msisdn}</i>) }</p>
<p>{ _t("A text message has been sent to %(msisdn)s",
{ msisdn: <i>this._msisdn</i> },
) }
</p>
<p>{ _t("Please enter the code it contains:") }</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}>

View file

@ -0,0 +1,59 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
'use strict';
import SettingsStore from "../../../settings/SettingsStore";
const React = require('react');
module.exports = React.createClass({
displayName: 'LoginPage',
render: function() {
// FIXME: this should be turned into a proper skin with a StatusLoginPage component
if (SettingsStore.getValue("theme") === 'status') {
return (
<div className="mx_StatusLogin">
<div className="mx_StatusLogin_brand">
<img src="themes/status/img/logo.svg" alt="Status" width="221" height="53" />
</div>
<div className="mx_StatusLogin_content">
<div className="mx_StatusLogin_header">
<h1>Status Community Chat</h1>
<div className="mx_StatusLogin_subtitle">
A safer, decentralised communication
platform <a href="https://riot.im">powered by Riot</a>
</div>
</div>
{ this.props.children }
<div className="mx_StatusLogin_footer">
<p>This channel is for our development community.</p>
<p>Interested in SNT and discussions on the cryptocurrency market?</p>
<p><a href="https://t.me/StatusNetworkChat" target="_blank" className="mx_StatusLogin_footer_cta">Join Telegram Chat</a></p>
</div>
</div>
</div>
);
} else {
return (
<div className="mx_Login">
{ this.props.children }
</div>
);
}
},
});

View file

@ -20,7 +20,7 @@ import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {field_input_incorrect} from '../../../UiEffects';
import SdkConfig from '../../../SdkConfig';
/**
* A pure UI component which displays a username/password form.
@ -144,7 +144,10 @@ class PasswordLogin extends React.Component {
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
placeholder={_t('User name')}
placeholder={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("User name")}
value={this.state.username}
autoFocus
disabled={disabled}
@ -210,9 +213,9 @@ class PasswordLogin extends React.Component {
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
return (
<div>
<form onSubmit={this.onSubmitForm}>
let loginType;
if (!SdkConfig.get().disable_3pid_login) {
loginType = (
<div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Dropdown
@ -225,6 +228,13 @@ class PasswordLogin extends React.Component {
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
</Dropdown>
</div>
);
}
return (
<div>
<form onSubmit={this.onSubmitForm}>
{ loginType }
{ loginField }
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
name="password"

View file

@ -22,6 +22,8 @@ import Email from '../../../email';
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_COUNTRY = 'field_phone_country';
@ -122,7 +124,7 @@ module.exports = React.createClass({
password: this.refs.password.value.trim(),
email: email,
phoneCountry: this.state.phoneCountry,
phoneNumber: this.refs.phoneNumber.value.trim(),
phoneNumber: this.refs.phoneNumber ? this.refs.phoneNumber.value.trim() : '',
});
if (promise) {
@ -180,7 +182,7 @@ module.exports = React.createClass({
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
break;
case FIELD_PHONE_NUMBER:
const phoneNumber = this.refs.phoneNumber.value;
const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : '';
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
break;
@ -273,10 +275,14 @@ module.exports = React.createClass({
render: function() {
const self = this;
const theme = SettingsStore.getValue("theme");
// FIXME: remove hardcoded Status team tweaks at some point
const emailPlaceholder = theme === 'status' ? _t("Email address") : _t("Email address (optional)");
const emailSection = (
<div>
<input type="text" ref="email"
autoFocus={true} placeholder={_t("Email address (optional)")}
autoFocus={true} placeholder={emailPlaceholder}
defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}}
@ -306,7 +312,9 @@ module.exports = React.createClass({
}
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
const phoneSection = (
let phoneSection;
if (!SdkConfig.get().disable_3pid_login) {
phoneSection = (
<div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
className="mx_Login_phoneCountry mx_Login_field_prefix"
@ -328,6 +336,7 @@ module.exports = React.createClass({
/>
</div>
);
}
const registerButton = (
<input className="mx_Login_submit" type="submit" value={_t("Register")} />

View file

@ -25,8 +25,8 @@ import sdk from '../../../index';
import dis from '../../../dispatcher';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({
displayName: 'MImageBody',
@ -81,7 +81,7 @@ module.exports = React.createClass({
},
onImageEnter: function(e) {
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return;
}
const imgElement = e.target;
@ -89,7 +89,7 @@ module.exports = React.createClass({
},
onImageLeave: function(e) {
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return;
}
const imgElement = e.target;
@ -218,7 +218,7 @@ module.exports = React.createClass({
const contentUrl = this._getContentUrl();
let thumbUrl;
if (this._isGif() && UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
thumbUrl = contentUrl;
} else {
thumbUrl = this._getThumbUrl();
@ -230,6 +230,7 @@ module.exports = React.createClass({
<a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body}
onLoad={this.props.onWidgetLoad}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>

View file

@ -21,8 +21,8 @@ import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({
displayName: 'MVideoBody',
@ -151,7 +151,7 @@ module.exports = React.createClass({
const contentUrl = this._getContentUrl();
const thumbUrl = this._getThumbUrl();
const autoplay = UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false);
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
let height = null;
let width = null;
let poster = null;

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk';
import { _t, _tJsx } from '../../../languageHandler';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
@ -67,24 +67,17 @@ module.exports = React.createClass({
'crop',
);
// it sucks that _tJsx doesn't support normal _t substitutions :((
return (
<div className="mx_RoomAvatarEvent">
{ _tJsx('%(senderDisplayName)s changed the room avatar to <img/>',
[
/%\(senderDisplayName\)s/,
/<img\/>/,
],
[
(sub) => senderDisplayName,
(sub) =>
{ _t('%(senderDisplayName)s changed the room avatar to <img/>',
{ senderDisplayName: senderDisplayName },
{
'img': () =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
onClick={this.onAvatarClick.bind(this, name)}>
<BaseAvatar width={14} height={14} url={url}
name={name} />
<BaseAvatar width={14} height={14} url={url} name={name} />
</AccessibleButton>,
],
)
})
}
</div>
);

View file

@ -17,53 +17,128 @@
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import Flair from '../elements/Flair.js';
import { _tJsx } from '../../../languageHandler';
import FlairStore from '../../../stores/FlairStore';
import { _t } from '../../../languageHandler';
export default function SenderProfile(props) {
export default React.createClass({
displayName: 'SenderProfile',
propTypes: {
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
text: PropTypes.string, // Text to show. Defaults to sender name
onClick: PropTypes.func,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getInitialState() {
return {
userGroups: null,
relatedGroups: [],
};
},
componentWillMount() {
this.unmounted = false;
this._updateRelatedGroups();
FlairStore.getPublicisedGroupsCached(
this.context.matrixClient, this.props.mxEvent.getSender(),
).then((userGroups) => {
if (this.unmounted) return;
this.setState({userGroups});
});
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
},
onRoomStateEvents(event) {
if (event.getType() === 'm.room.related_groups' &&
event.getRoomId() === this.props.mxEvent.getRoomId()
) {
this._updateRelatedGroups();
}
},
_updateRelatedGroups() {
if (this.unmounted) return;
const relatedGroupsEvent = this.context.matrixClient
.getRoom(this.props.mxEvent.getRoomId())
.currentState
.getStateEvents('m.room.related_groups', '');
this.setState({
relatedGroups: relatedGroupsEvent ?
relatedGroupsEvent.getContent().groups || []
: [],
});
},
_getDisplayedGroups(userGroups, relatedGroups) {
let displayedGroups = userGroups || [];
if (relatedGroups && relatedGroups.length > 0) {
displayedGroups = displayedGroups.filter((groupId) => {
return relatedGroups.includes(groupId);
});
} else {
displayedGroups = [];
}
return displayedGroups;
},
render() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const {mxEvent} = props;
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
const {mxEvent} = this.props;
let name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
const {msgtype} = mxEvent.getContent();
if (msgtype === 'm.emote') {
return <span />; // emote message must include the name so don't duplicate it
}
// Name + flair
const nameElem = [
<EmojiText key='name' className="mx_SenderProfile_name">{ name || '' }</EmojiText>,
props.enableFlair ?
<Flair key='flair'
let flair = <div />;
if (this.props.enableFlair) {
const displayedGroups = this._getDisplayedGroups(
this.state.userGroups, this.state.relatedGroups,
);
// Backwards-compatible replacing of "(IRC)" with AS user flair
name = displayedGroups.length > 0 ? name.replace(' (IRC)', '') : name;
flair = <Flair key='flair'
userId={mxEvent.getSender()}
roomId={mxEvent.getRoomId()}
showRelated={true} />
: null,
];
let content = '';
if(props.text) {
// Replace senderName, and wrap surrounding text in spans with the right class
content = _tJsx(props.text, /^(.*)\%\(senderName\)s(.*)$/m, (p1, p2) => [
p1 ? <span className='mx_SenderProfile_aux'>{ p1 }</span> : null,
nameElem,
p2 ? <span className='mx_SenderProfile_aux'>{ p2 }</span> : null,
]);
} else {
content = nameElem;
groups={displayedGroups}
/>;
}
const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>;
// Name + flair
const nameFlair = <span>
<span className="mx_SenderProfile_name">
{ nameElem }
</span>
{ flair }
</span>;
const content = this.props.text ?
<span className="mx_SenderProfile_aux">
{ _t(this.props.text, { senderName: () => nameElem }) }
</span> : nameFlair;
return (
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
{ content }
</div>
);
}
SenderProfile.propTypes = {
mxEvent: React.PropTypes.object.isRequired, // event whose sender we're showing
text: React.PropTypes.string, // Text to show. Defaults to sender name
onClick: React.PropTypes.func,
};
},
});

View file

@ -29,11 +29,11 @@ import Modal from '../../../Modal';
import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import UserSettingsStore from "../../../UserSettingsStore";
import MatrixClientPeg from '../../../MatrixClientPeg';
import ContextualMenu from '../../structures/ContextualMenu';
import {RoomMember} from 'matrix-js-sdk';
import classNames from 'classnames';
import SettingsStore from "../../../settings/SettingsStore";
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
linkifyMatrix(linkify);
@ -104,7 +104,7 @@ module.exports = React.createClass({
setTimeout(() => {
if (this._unmounted) return;
for (let i = 0; i < blocks.length; i++) {
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
highlight.highlightBlock(blocks[i]);
} else {
// Only syntax highlight if there's a class starting with language-
@ -169,7 +169,7 @@ module.exports = React.createClass({
},
pillifyLinks: function(nodes) {
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
let node = nodes[0];
while (node) {
let pillified = false;
@ -224,7 +224,7 @@ module.exports = React.createClass({
if (roomNotifTextNodes.length > 0) {
const pushProcessor = new PushProcessor(MatrixClientPeg.get());
const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif");
if (pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) {
if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) {
// Now replace all those nodes with Pills
for (const roomNotifTextNode of roomNotifTextNodes) {
const pillContainer = document.createElement('span');
@ -419,7 +419,7 @@ module.exports = React.createClass({
const content = mxEvent.getContent();
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
disableBigEmoji: UserSettingsStore.getSyncedSetting('TextualBody.disableBigEmoji', false),
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
});
if (this.props.highlightLink) {

View file

@ -22,10 +22,11 @@ const MatrixClientPeg = require("../../../MatrixClientPeg");
const Modal = require("../../../Modal");
import dis from '../../../dispatcher';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
const ROOM_COLORS = [
// magic room default values courtesy of Ribot
["#76cfa6", "#eaf5f0"],
[Tinter.getKeyRgb()[0], Tinter.getKeyRgb()[1]],
["#81bddb", "#eaf1f4"],
["#bd79cb", "#f3eaf5"],
["#c65d94", "#f5eaef"],
@ -47,17 +48,17 @@ module.exports = React.createClass({
getInitialState: function() {
const data = {
index: 0,
primary_color: ROOM_COLORS[0].primary_color,
secondary_color: ROOM_COLORS[0].secondary_color,
primary_color: ROOM_COLORS[0][0],
secondary_color: ROOM_COLORS[0][1],
hasChanged: false,
};
const event = this.props.room.getAccountData("org.matrix.room.color_scheme");
if (!event) {
return data;
}
const scheme = event.getContent();
const scheme = SettingsStore.getValueAt(SettingLevel.ROOM_ACCOUNT, "roomColor", this.props.room.roomId);
if (scheme.primary_color && scheme.secondary_color) {
// We only use the user's scheme if the scheme is valid.
data.primary_color = scheme.primary_color;
data.secondary_color = scheme.secondary_color;
}
data.index = this._getColorIndex(data);
if (data.index === -1) {
@ -81,13 +82,13 @@ module.exports = React.createClass({
// We would like guests to be able to set room colour but currently
// they can't, so we still send the request but display a sensible
// error if it fails.
return MatrixClientPeg.get().setRoomAccountData(
this.props.room.roomId, "org.matrix.room.color_scheme", {
// TODO: Support guests for room color. Technically this is possible via granular settings
// Granular settings would mean the guest is forced to use the DEVICE level though.
SettingsStore.setValue("roomColor", this.props.room.roomId, SettingLevel.ROOM_ACCOUNT, {
primary_color: this.state.primary_color,
secondary_color: this.state.secondary_color,
},
).catch(function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
}).catch(function(err) {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
dis.dispatch({action: 'view_set_mxid'});
}
});

View file

@ -104,8 +104,8 @@ module.exports = React.createClass({
render: function() {
const localDomain = this.context.matrixClient.getDomain();
const EditableItemList = sdk.getComponent('elements.EditableItemList');
return (<div>
<h3>{ _t('Related Communities') }</h3>
return <div>
<h3>{ _t('Flair') }</h3>
<EditableItemList
items={this.state.newGroupsList}
className={"mx_RelatedGroupSettings"}
@ -115,12 +115,12 @@ module.exports = React.createClass({
onItemAdded={this.onGroupAdded}
onItemEdited={this.onGroupEdited}
onItemRemoved={this.onGroupDeleted}
itemsLabel={_t('Related communities for this room:')}
noItemsLabel={_t('This room has no related communities')}
itemsLabel={_t('Showing flair for these communities:')}
noItemsLabel={_t('This room is not showing flair for any communities')}
placeholder={_t(
'New community ID (e.g. +foo:%(localDomain)s)', {localDomain},
)}
/>
</div>);
</div>;
},
});

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@ -14,13 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
const React = require('react');
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require("../../../index");
const Modal = require("../../../Modal");
const UserSettingsStore = require('../../../UserSettingsStore');
import { _t, _tJsx } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
module.exports = React.createClass({
@ -30,137 +28,64 @@ module.exports = React.createClass({
room: React.PropTypes.object,
},
getInitialState: function() {
const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState;
const roomPreviewUrls = this.props.room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
const userPreviewUrls = this.props.room.getAccountData("org.matrix.room.preview_urls");
return {
globalDisableUrlPreview: (roomPreviewUrls && roomPreviewUrls.getContent().disable) || false,
userDisableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === true)) || false,
userEnableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === false)) || false,
};
},
componentDidMount: function() {
this.originalState = Object.assign({}, this.state);
},
saveSettings: function() {
const promises = [];
if (this.state.globalDisableUrlPreview !== this.originalState.globalDisableUrlPreview) {
console.log("UrlPreviewSettings: Updating room's preview_urls state event");
promises.push(
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId, "org.matrix.room.preview_urls", {
disable: this.state.globalDisableUrlPreview,
}, "",
),
);
}
let content = undefined;
if (this.state.userDisableUrlPreview !== this.originalState.userDisableUrlPreview) {
console.log("UrlPreviewSettings: Disabling user's per-room preview_urls");
content = this.state.userDisableUrlPreview ? { disable: true } : {};
}
if (this.state.userEnableUrlPreview !== this.originalState.userEnableUrlPreview) {
console.log("UrlPreviewSettings: Enabling user's per-room preview_urls");
if (!content || content.disable === undefined) {
content = this.state.userEnableUrlPreview ? { disable: false } : {};
}
}
if (content) {
promises.push(
MatrixClientPeg.get().setRoomAccountData(
this.props.room.roomId, "org.matrix.room.preview_urls", content,
),
);
}
console.log("UrlPreviewSettings: saveSettings: " + JSON.stringify(promises));
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
if (this.refs.urlPreviewsSelf) promises.push(this.refs.urlPreviewsSelf.save());
return promises;
},
onGlobalDisableUrlPreviewChange: function() {
this.setState({
globalDisableUrlPreview: this.refs.globalDisableUrlPreview.checked ? true : false,
});
},
onUserEnableUrlPreviewChange: function() {
this.setState({
userDisableUrlPreview: false,
userEnableUrlPreview: this.refs.userEnableUrlPreview.checked ? true : false,
});
},
onUserDisableUrlPreviewChange: function() {
this.setState({
userDisableUrlPreview: this.refs.userDisableUrlPreview.checked ? true : false,
userEnableUrlPreview: false,
});
},
render: function() {
const self = this;
const roomState = this.props.room.currentState;
const cli = MatrixClientPeg.get();
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const roomId = this.props.room.roomId;
const maySetRoomPreviewUrls = roomState.mayClientSendStateEvent('org.matrix.room.preview_urls', cli);
let disableRoomPreviewUrls;
if (maySetRoomPreviewUrls) {
disableRoomPreviewUrls =
<label>
<input type="checkbox" ref="globalDisableUrlPreview"
onChange={this.onGlobalDisableUrlPreviewChange}
checked={this.state.globalDisableUrlPreview} />
{ _t("Disable URL previews by default for participants in this room") }
</label>;
} else {
disableRoomPreviewUrls =
<label>
{ _t("URL previews are %(globalDisableUrlPreview)s by default for participants in this room.", {globalDisableUrlPreview: this.state.globalDisableUrlPreview ? _t("disabled") : _t("enabled")}) }
</label>;
}
let urlPreviewText = null;
if (UserSettingsStore.getUrlPreviewsDisabled()) {
urlPreviewText = (
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
let previewsForAccount = null;
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
previewsForAccount = (
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
);
} else {
urlPreviewText = (
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
previewsForAccount = (
_t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
);
}
let previewsForRoom = null;
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
previewsForRoom = (
<label>
<SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM}
roomId={this.props.room.roomId}
isExplicit={true}
manualSave={true}
ref="urlPreviewsRoom" />
</label>
);
} else {
let str = _td("URL previews are enabled by default for participants in this room.");
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
str = _td("URL previews are disabled by default for participants in this room.");
}
previewsForRoom = (<label>{ _t(str) }</label>);
}
const previewsForRoomAccount = (
<SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM_ACCOUNT}
roomId={this.props.room.roomId}
manualSave={true}
ref="urlPreviewsSelf"
/>
);
return (
<div className="mx_RoomSettings_toggles">
<h3>{ _t("URL Previews") }</h3>
<label>
{ urlPreviewText }
</label>
{ disableRoomPreviewUrls }
<label>
<input type="checkbox" ref="userEnableUrlPreview"
onChange={this.onUserEnableUrlPreviewChange}
checked={this.state.userEnableUrlPreview} />
{ _t("Enable URL previews for this room (affects only you)") }
</label>
<label>
<input type="checkbox" ref="userDisableUrlPreview"
onChange={this.onUserDisableUrlPreviewChange}
checked={this.state.userDisableUrlPreview} />
{ _t("Disable URL previews for this room (affects only you)") }
</label>
<label>{ previewsForAccount }</label>
{ previewsForRoom }
<label>{ previewsForRoomAccount }</label>
</div>
);
},

View file

@ -24,9 +24,10 @@ import isEqual from 'lodash/isEqual';
import sdk from '../../../index';
import type {Completion} from '../../../autocomplete/Autocompleter';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { Room } from 'matrix-js-sdk';
import {getCompletions} from '../../../autocomplete/Autocompleter';
import SettingsStore from "../../../settings/SettingsStore";
import Autocompleter from '../../../autocomplete/Autocompleter';
const COMPOSER_SELECTED = 0;
@ -95,7 +96,7 @@ export default class Autocomplete extends React.Component {
});
return Promise.resolve(null);
}
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
let autocompleteDelay = SettingsStore.getValue("autocompleteDelay");
// Don't debounce if we are already showing completions
if (this.state.completions.length > 0 || this.state.forceComplete) {

View file

@ -21,8 +21,7 @@ import sdk from '../../../index';
import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils';
import AppsDrawer from './AppsDrawer';
import { _t, _tJsx} from '../../../languageHandler';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -100,13 +99,13 @@ module.exports = React.createClass({
supportedText = _t(" (unsupported)");
} else {
joinNode = (<span>
{ _tJsx(
{ _t(
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
[/<voiceText>(.*?)<\/voiceText>/, /<videoText>(.*?)<\/videoText>/],
[
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
],
{},
{
'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
},
) }
</span>);
}

View file

@ -33,22 +33,30 @@ const ObjectUtils = require('../../../ObjectUtils');
const eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
'm.room.member': 'messages.TextualEvent',
'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent',
};
const stateEventTileTypes = {
'm.room.member': 'messages.TextualEvent',
'm.room.name': 'messages.TextualEvent',
'm.room.avatar': 'messages.RoomAvatarEvent',
'm.room.topic': 'messages.TextualEvent',
'm.room.third_party_invite': 'messages.TextualEvent',
'm.room.history_visibility': 'messages.TextualEvent',
'm.room.encryption': 'messages.TextualEvent',
'm.room.topic': 'messages.TextualEvent',
'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent',
'im.vector.modular.widgets': 'messages.TextualEvent',
};
function getHandlerTile(ev) {
const type = ev.getType();
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
}
const MAX_READ_AVATARS = 5;
// Our component structure for EventTiles on the timeline is:
@ -433,7 +441,7 @@ module.exports = withMatrixClient(React.createClass({
// Info messages are basically information about commands processed on a room
const isInfoMessage = (eventType !== 'm.room.message');
const EventTileType = sdk.getComponent(eventTileTypes[eventType]);
const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!EventTileType) {
@ -600,8 +608,10 @@ module.exports = withMatrixClient(React.createClass({
module.exports.haveTileForEvent = function(e) {
// Only messages have a tile (black-rectangle) if redacted
if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
if (eventTileTypes[e.getType()] == undefined) return false;
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
const handler = getHandlerTile(e);
if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') {
return TextForEvent.textForEvent(e) !== '';
} else {
return true;

View file

@ -18,7 +18,7 @@
import React from 'react';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode';
import { KeyCode } from '../../../Keyboard';
module.exports = React.createClass({

View file

@ -494,7 +494,6 @@ module.exports = withMatrixClient(React.createClass({
const defaultPerms = {
can: {},
muted: false,
modifyLevel: false,
};
const room = this.props.matrixClient.getRoom(member.roomId);
if (!room) return defaultPerms;
@ -516,13 +515,15 @@ module.exports = withMatrixClient(React.createClass({
},
_calculateCanPermissions: function(me, them, powerLevels) {
const isMe = me.userId === them.userId;
const can = {
kick: false,
ban: false,
mute: false,
modifyLevel: false,
modifyLevelMax: 0,
};
const canAffectUser = them.powerLevel < me.powerLevel;
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
if (!canAffectUser) {
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
return can;
@ -531,16 +532,13 @@ module.exports = withMatrixClient(React.createClass({
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
powerLevels.state_default
);
const levelToSend = (
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
powerLevels.events_default
);
can.kick = me.powerLevel >= powerLevels.kick;
can.ban = me.powerLevel >= powerLevels.ban;
can.mute = me.powerLevel >= editPowerLevel;
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
can.modifyLevel = me.powerLevel > them.powerLevel && me.powerLevel >= editPowerLevel;
can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
can.modifyLevelMax = me.powerLevel;
return can;
},
@ -832,8 +830,11 @@ module.exports = withMatrixClient(React.createClass({
presenceCurrentlyActive = this.props.member.user.currentlyActive;
}
let roomMemberDetails = null;
const room = this.props.matrixClient.getRoom(this.props.member.roomId);
const poweLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
const powerLevelUsersDefault = poweLevelEvent.getContent().users_default;
let roomMemberDetails = null;
if (this.props.member.roomId) { // is in room
const PowerSelector = sdk.getComponent('elements.PowerSelector');
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
@ -842,7 +843,9 @@ module.exports = withMatrixClient(React.createClass({
{ _t("Level:") } <b>
<PowerSelector controlled={true}
value={parseInt(this.props.member.powerLevel)}
maxValue={this.state.can.modifyLevelMax}
disabled={!this.state.can.modifyLevel}
usersDefault={powerLevelUsersDefault}
onChange={this.onPowerChange} />
</b>
</div>

View file

@ -22,7 +22,7 @@ import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import Autocomplete from './Autocomplete';
import UserSettingsStore from '../../../UserSettingsStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
export default class MessageComposer extends React.Component {
@ -49,10 +49,10 @@ export default class MessageComposer extends React.Component {
inputState: {
style: [],
blockType: null,
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false),
isRichtextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
wordCount: 0,
},
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
};
}
@ -226,7 +226,7 @@ export default class MessageComposer extends React.Component {
}
onToggleFormattingClicked() {
UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting);
SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting);
this.setState({showFormatting: !this.state.showFormatting});
}
@ -238,7 +238,7 @@ export default class MessageComposer extends React.Component {
render() {
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
const uploadInputStyle = {display: 'none'};
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const MemberPresenceAvatar = sdk.getComponent('avatars.MemberPresenceAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
@ -246,7 +246,7 @@ export default class MessageComposer extends React.Component {
controls.push(
<div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} />
<MemberPresenceAvatar member={me} width={24} height={24} />
</div>,
);

View file

@ -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,7 +425,7 @@ 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) {
@ -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 => {

View file

@ -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) {
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 (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) } { ago }
{ this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive) }
</div>
);
} else {
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) }
</div>
);
}
},
});

View file

@ -31,7 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader';
import UserSettingsStore from "../../../UserSettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
linkifyMatrix(linkify);
@ -339,7 +339,7 @@ module.exports = React.createClass({
</AccessibleButton>;
}
if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) {
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
let pinsIndicator = null;
if (this._hasUnreadPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);

View file

@ -18,18 +18,18 @@ limitations under the License.
'use strict';
const React = require("react");
const ReactDOM = require("react-dom");
import { _t, _tJsx } from '../../../languageHandler';
import { _t } from '../../../languageHandler';
const GeminiScrollbar = require('react-gemini-scrollbar');
const MatrixClientPeg = require("../../../MatrixClientPeg");
const CallHandler = require('../../../CallHandler');
const RoomListSorter = require("../../../RoomListSorter");
const Unread = require('../../../Unread');
const dis = require("../../../dispatcher");
const sdk = require('../../../index');
const rate_limited_func = require('../../../ratelimitedfunc');
const Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap';
const Receipt = require('../../../utils/Receipt');
import FilterStore from '../../../stores/FilterStore';
import GroupStoreCache from '../../../stores/GroupStoreCache';
const HIDE_CONFERENCE_CHANS = true;
@ -63,6 +63,7 @@ module.exports = React.createClass({
totalRoomCount: null,
lists: {},
incomingCall: null,
selectedTags: [],
};
},
@ -82,6 +83,23 @@ module.exports = React.createClass({
cli.on("accountData", this.onAccountData);
cli.on("Group.myMembership", this._onGroupMyMembership);
this._groupStores = {};
this._selectedTagsRoomIds = [];
this._selectedTagsUserIds = [];
// When the selected tags are changed, initialise a group store if necessary
this._filterStoreToken = FilterStore.addListener(() => {
FilterStore.getSelectedTags().forEach((tag) => {
if (tag[0] !== '+' || this._groupStores[tag]) {
return;
}
this._groupStores[tag] = GroupStoreCache.getGroupStore(tag);
this._groupStores[tag].registerListener(() => {
this.updateSelectedTagsEntities();
});
});
this.updateSelectedTagsEntities();
});
this.refreshRoomList();
// order of the sublists
@ -150,6 +168,11 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
}
if (this._filterStoreToken) {
this._filterStoreToken.remove();
}
// cancel any pending calls to the rate_limited_funcs
this._delayedRefreshRoomList.cancelPendingCall();
},
@ -236,6 +259,41 @@ module.exports = React.createClass({
this.refreshRoomList();
}, 500),
// Update which rooms and users should appear in RoomList as dictated by selected tags
updateSelectedTagsEntities: function() {
if (!this.mounted) return;
this._selectedTagsRoomIds = [];
this._selectedTagsUserIds = [];
FilterStore.getSelectedTags().forEach((tag) => {
this._selectedTagsRoomIds = this._selectedTagsRoomIds.concat(
this._groupStores[tag].getGroupRooms().map((room) => room.roomId),
);
// TODO: Check if room has been tagged to the group by the user
this._selectedTagsUserIds = this._selectedTagsUserIds.concat(
this._groupStores[tag].getGroupMembers().map((member) => member.userId),
);
});
this.setState({
selectedTags: FilterStore.getSelectedTags(),
}, () => {
this.refreshRoomList();
});
},
isRoomInSelectedTags: function(room, me, dmRoomMap) {
// No selected tags = every room is visible in the list
if (this.state.selectedTags.length === 0) {
return true;
}
if (this._selectedTagsRoomIds.includes(room.roomId)) {
return true;
}
const dmUserId = dmRoomMap.getUserIdForRoomId(room.roomId);
return dmUserId && dmUserId !== me.userId &&
this._selectedTagsUserIds.includes(dmUserId);
},
refreshRoomList: function() {
// TODO: ideally we'd calculate this once at start, and then maintain
// any changes to it incrementally, updating the appropriate sublists
@ -255,9 +313,7 @@ module.exports = React.createClass({
},
getRoomLists: function() {
const self = this;
const lists = {};
lists["im.vector.fake.invite"] = [];
lists["m.favourite"] = [];
lists["im.vector.fake.recent"] = [];
@ -266,8 +322,7 @@ module.exports = React.createClass({
lists["im.vector.fake.archived"] = [];
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
MatrixClientPeg.get().getRooms().forEach(function(room) {
MatrixClientPeg.get().getRooms().forEach((room) => {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) return;
@ -278,13 +333,18 @@ module.exports = React.createClass({
if (me.membership == "invite") {
lists["im.vector.fake.invite"].push(room);
} else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
} else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, this.props.ConferenceHandler)) {
// skip past this room & don't put it in any lists
} else if (me.membership == "join" || me.membership === "ban" ||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
// Used to split rooms via tags
const tagNames = Object.keys(room.tags);
// Apply TagPanel filtering, derived from FilterStore
if (!this.isRoomInSelectedTags(room, me, dmRoomMap)) {
return;
}
if (tagNames.length) {
for (let i = 0; i < tagNames.length; i++) {
const tagName = tagNames[i];
@ -476,6 +536,10 @@ module.exports = React.createClass({
},
_getEmptyContent: function(section) {
if (this.state.selectedTags.length > 0) {
return null;
}
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
if (this.props.collapsed) {
@ -486,28 +550,25 @@ module.exports = React.createClass({
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
switch (section) {
case 'im.vector.fake.direct':
return <div className="mx_RoomList_emptySubListTip">
{ _tJsx(
{ _t(
"Press <StartChatButton> to start a chat with someone",
[/<StartChatButton>/],
[
(sub) => <StartChatButton size="16" callout={true} />,
],
{},
{ 'StartChatButton': <StartChatButton size="16" callout={true} /> },
) }
</div>;
case 'im.vector.fake.recent':
return <div className="mx_RoomList_emptySubListTip">
{ _tJsx(
{ _t(
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
" <RoomDirectoryButton> to browse the directory",
[/<CreateRoomButton>/, /<RoomDirectoryButton>/],
[
(sub) => <CreateRoomButton size="16" callout={true} />,
(sub) => <RoomDirectoryButton size="16" callout={true} />,
],
{},
{
'CreateRoomButton': <CreateRoomButton size="16" callout={true} />,
'RoomDirectoryButton': <RoomDirectoryButton size="16" callout={true} />,
},
) }
</div>;
}

View file

@ -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}) }
</div>
<div className="mx_RoomPreviewBar_join_text">
{ _tJsx(
{ _t(
'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?',
[/<acceptText>(.*?)<\/acceptText>/, /<declineText>(.*?)<\/declineText>/],
[
(sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>,
(sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>,
],
{},
{
'acceptText': (sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>,
'declineText': (sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>,
},
) }
</div>
{ emailMatchBlock }
@ -211,9 +211,9 @@ module.exports = React.createClass({
<div className="mx_RoomPreviewBar_join_text">
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
<br />
{ _tJsx("<a>Click here</a> to join the discussion!",
/<a>(.*?)<\/a>/,
(sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a>,
{ _t("<a>Click here</a> to join the discussion!",
{},
{ 'a': (sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a> },
) }
</div>
</div>

View file

@ -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);
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 =
<label>
<input type="checkbox" ref="blacklistUnverified"
defaultChecked={isGlobalBlacklistUnverified || isRoomBlacklistUnverified}
disabled={isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked)} />
{ _t('Never send encrypted messages to unverified devices in this room from this device') }.
</label>;
const settings = (
<SettingsFlag name="blacklistUnverifiedDevices"
level={SettingLevel.ROOM_DEVICE}
roomId={this.props.room.roomId}
manualSave={true}
ref="blacklistUnverifiedDevices"
/>
);
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 = <RelatedGroupSettings ref="related_groups"
const relatedGroupsSection = <RelatedGroupSettings ref="related_groups"
roomId={this.props.room.roomId}
canSetRelatedGroups={roomState.mayClientSendStateEvent("m.room.related_groups", cli)}
relatedGroupsEvent={this.props.room.currentState.getStateEvents('m.room.related_groups', '')} />;
}
relatedGroupsEvent={this.props.room.currentState.getStateEvents('m.room.related_groups', '')}
/>;
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 =
<div className="mx_RoomSettings_tags">
{ _t("Tagged as: ") }{ canSetTag ?
(tags.map(function(tag, i) {
@ -792,10 +779,10 @@ module.exports = React.createClass({
if (this.state.join_rule === "public" && aliasCount == 0) {
addressWarning =
<div className="mx_RoomSettings_warning">
{ _tJsx(
{ _t(
'To link to a room it must have <a>an address</a>.',
/<a>(.*?)<\/a>/,
(sub) => <a href="#addresses">{ sub }</a>,
{},
{ 'a': (sub) => <a href="#addresses">{ sub }</a> },
) }
</div>;
}
@ -912,41 +899,41 @@ module.exports = React.createClass({
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span>
<PowerSelector ref="users_default" value={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged} />
<PowerSelector ref="users_default" value={default_user_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages, you must be a') } </span>
<PowerSelector ref="events_default" value={send_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged} />
<PowerSelector ref="events_default" value={send_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room, you must be a') } </span>
<PowerSelector ref="invite" value={invite_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} />
<PowerSelector ref="invite" value={invite_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room, you must be a') } </span>
<PowerSelector ref="state_default" value={state_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} />
<PowerSelector ref="state_default" value={state_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users, you must be a') } </span>
<PowerSelector ref="kick" value={kick_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} />
<PowerSelector ref="kick" value={kick_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users, you must be a') } </span>
<PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} />
<PowerSelector ref="ban" value={ban_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} />
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages, you must be a') } </span>
<PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} />
<PowerSelector ref="redact" value={redact_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} />
</div>
{ 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 <eventType/>, you must be a", /<eventType\/>/, () => <code>{ event_type }</code>);
else label = _t("To send events of type <eventType/>, you must be a", {}, { 'eventType': <code>{ event_type }</code> });
return (
<div className="mx_RoomSettings_powerLevel" key={event_type}>
<span className="mx_RoomSettings_powerLevelKey">{ label } </span>
<PowerSelector ref={"event_levels_"+event_type} value={events_levels[event_type]} onChange={self.onPowerLevelsChanged}
<PowerSelector ref={"event_levels_"+event_type} value={events_levels[event_type]} usersDefault={default_user_level} onChange={self.onPowerLevelsChanged}
controlled={false} disabled={!can_change_levels || current_user_level < events_levels[event_type]} />
</div>
);

View file

@ -27,7 +27,6 @@ const ContextualMenu = require('../../structures/ContextualMenu');
const RoomNotifs = require('../../../RoomNotifs');
const FormattingUtils = require('../../../utils/FormattingUtils');
import AccessibleButton from '../elements/AccessibleButton';
const UserSettingsStore = require('../../../UserSettingsStore');
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';

View file

@ -20,7 +20,7 @@ import classNames from 'classnames';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
export default class DevicesPanel extends React.Component {
constructor(props, context) {
@ -29,11 +29,16 @@ export default class DevicesPanel extends React.Component {
this.state = {
devices: undefined,
deviceLoadError: undefined,
selectedDevices: [],
deleting: false,
};
this._unmounted = false;
this._renderDevice = this._renderDevice.bind(this);
this._onDeviceSelectionToggled = this._onDeviceSelectionToggled.bind(this);
this._onDeleteClick = this._onDeleteClick.bind(this);
}
componentDidMount() {
@ -82,25 +87,78 @@ export default class DevicesPanel extends React.Component {
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
}
_onDeviceDeleted(device) {
_onDeviceSelectionToggled(device) {
if (this._unmounted) { return; }
// delete the removed device from our list.
const removed_id = device.device_id;
const deviceId = device.device_id;
this.setState((state, props) => {
const newDevices = state.devices.filter(
(d) => { return d.device_id != removed_id; },
);
return { devices: newDevices };
// Make a copy of the selected devices, then add or remove the device
const selectedDevices = state.selectedDevices.slice();
const i = selectedDevices.indexOf(deviceId);
if (i === -1) {
selectedDevices.push(deviceId);
} else {
selectedDevices.splice(i, 1);
}
return {selectedDevices};
});
}
_onDeleteClick() {
this.setState({
deleting: true,
});
this._makeDeleteRequest(null).catch((error) => {
if (this._unmounted) { return; }
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
// doesn't look like an interactive-auth failure
throw error;
}
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
title: _t("Authentication"),
matrixClient: MatrixClientPeg.get(),
authData: error.data,
makeRequest: this._makeDeleteRequest.bind(this),
});
}).catch((e) => {
console.error("Error deleting devices", e);
if (this._unmounted) { return; }
}).finally(() => {
this.setState({
deleting: false,
});
});
}
_makeDeleteRequest(auth) {
return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then(
() => {
// Remove the deleted devices from `devices`, reset selection to []
this.setState({
devices: this.state.devices.filter(
(d) => !this.state.selectedDevices.includes(d.device_id),
),
selectedDevices: [],
});
},
);
}
_renderDevice(device) {
const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
return (
<DevicesPanelEntry key={device.device_id} device={device}
onDeleted={()=>{this._onDeviceDeleted(device);}} />
);
return <DevicesPanelEntry
key={device.device_id}
device={device}
selected={this.state.selectedDevices.includes(device.device_id)}
onDeviceToggled={this._onDeviceSelectionToggled}
/>;
}
render() {
@ -124,6 +182,12 @@ export default class DevicesPanel extends React.Component {
devices.sort(this._deviceCompare);
const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> :
<div className="mx_textButton" onClick={this._onDeleteClick}>
{ _t("Delete %(count)s devices", {count: this.state.selectedDevices.length}) }
</div>;
const classes = classNames(this.props.className, "mx_DevicesPanel");
return (
<div className={classes}>
@ -131,7 +195,9 @@ export default class DevicesPanel extends React.Component {
<div className="mx_DevicesPanel_deviceId">{ _t("Device ID") }</div>
<div className="mx_DevicesPanel_deviceName">{ _t("Device Name") }</div>
<div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</div>
<div className="mx_DevicesPanel_deviceButtons"></div>
<div className="mx_DevicesPanel_deviceButtons">
{ this.state.selectedDevices.length > 0 ? deleteButton : _t('Select devices') }
</div>
</div>
{ devices.map(this._renderDevice) }
</div>

View file

@ -19,24 +19,15 @@ import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import DateUtils from '../../../DateUtils';
const AUTH_CACHE_AGE = 5 * 60 * 1000; // 5 minutes
export default class DevicesPanelEntry extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
deleting: false,
deleteError: undefined,
};
this._unmounted = false;
this._onDeleteClick = this._onDeleteClick.bind(this);
this.onDeviceToggled = this.onDeviceToggled.bind(this);
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
this._makeDeleteRequest = this._makeDeleteRequest.bind(this);
}
componentWillUnmount() {
@ -53,56 +44,8 @@ export default class DevicesPanelEntry extends React.Component {
});
}
_onDeleteClick() {
this.setState({deleting: true});
if (this.context.authCache.lastUpdate < Date.now() - AUTH_CACHE_AGE) {
this.context.authCache.auth = null;
}
// try with auth cache (which is null, so no interactive auth, to start off)
this._makeDeleteRequest(this.context.authCache.auth).catch((error) => {
if (this._unmounted) { return; }
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
// doesn't look like an interactive-auth failure
throw error;
}
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
title: _t("Authentication"),
matrixClient: MatrixClientPeg.get(),
authData: error.data,
makeRequest: this._makeDeleteRequest,
});
this.setState({
deleting: false,
});
}).catch((e) => {
console.error("Error deleting device", e);
if (this._unmounted) { return; }
this.setState({
deleting: false,
deleteError: _t("Failed to delete device"),
});
}).done();
}
_makeDeleteRequest(auth) {
this.context.authCache.auth = auth;
this.context.authCache.lastUpdate = Date.now();
const device = this.props.device;
return MatrixClientPeg.get().deleteDevice(device.device_id, auth).then(
() => {
this.props.onDeleted();
if (this._unmounted) { return; }
this.setState({ deleting: false });
},
);
onDeviceToggled() {
this.props.onDeviceToggled(this.props.device);
}
render() {
@ -110,16 +53,6 @@ export default class DevicesPanelEntry extends React.Component {
const device = this.props.device;
if (this.state.deleting) {
const Spinner = sdk.getComponent("elements.Spinner");
return (
<div className="mx_DevicesPanel_device">
<Spinner />
</div>
);
}
let lastSeen = "";
if (device.last_seen_ts) {
const lastSeenDate = DateUtils.formatDate(new Date(device.last_seen_ts));
@ -127,18 +60,6 @@ export default class DevicesPanelEntry extends React.Component {
lastSeenDate.toLocaleString();
}
let deleteButton;
if (this.state.deleteError) {
deleteButton = <div className="error">{ this.state.deleteError }</div>;
} else {
deleteButton = (
<div className="mx_textButton"
onClick={this._onDeleteClick}>
{ _t("Delete") }
</div>
);
}
let myDeviceClass = '';
if (device.device_id === MatrixClientPeg.get().getDeviceId()) {
myDeviceClass = " mx_DevicesPanel_myDevice";
@ -159,7 +80,7 @@ export default class DevicesPanelEntry extends React.Component {
{ lastSeen }
</div>
<div className="mx_DevicesPanel_deviceButtons">
{ deleteButton }
<input type="checkbox" onClick={this.onDeviceToggled} checked={this.props.selected} />
</div>
</div>
);
@ -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() {},
};

View file

@ -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 (

View file

@ -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 <a>enable unsafe scripts</a>.": "Nelze se připojit k homeserveru přes HTTP pokud je v adresním řádku HTTPS. Buď použijte HTTPS nebo <a>povolte nebezpečné scripty</a>.",
"%(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 <a>enable unsafe scripts</a>.": "Nelze se připojit k domovskému serveru přes HTTP, pokud je v adresním řádku HTTPS. Buď použijte HTTPS, nebo <a>povolte nebezpečné scripty</a>.",
"%(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 <img/>": "%(senderDisplayName)s změnil/a avatar místnosti na <img/>",
"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 <StartChatButton> to start a chat with someone": "Zmáčkněte <StartChatButton> 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 <a>an address</a>.": "Aby šlo odkazovat na místnost, musí mít <a>adresu</a>.",
"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 <eventType/>, you must be a": "Abyste mohl/a odesílat události typu <eventType/>, 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 <a>disabled</a> URL previews by default.": "<a>Vypnul/a</a> jste automatické náhledy webových adres.",
"You have <a>enabled</a> URL previews by default.": "<a>Zapnul/a</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 <a>web browser</a>": "Ochrana před roboty není aktuálně na desktopu dostupná. Použijte prosím <a>webový prohlížeč</a>",
"An email has been sent to %(emailAddress)s": "Na adresu %(emailAddress)s jsme poslali e-mail"
}

View file

@ -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.",
@ -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 <a>an address</a>.": "Um einen Raum zu verlinken, muss er <a>eine Adresse</a> 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 <a>web browser</a>": "In der Desktop-Version kann derzeit nicht geprüft werden, ob ein Benutzer ein Roboter ist. Bitte einen <a>Webbrowser</a> 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 <eventType/>, you must be a": "Um Ereignisse desTyps <eventType/> 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 <eventType/>, you must be a": "Notwendiges Berechtigungslevel, um Ereignisse des Typs <eventType/> 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 <i>+example:matrix.org</i>.": "Um einer bereits bestehenden Community beitreten zu können, musst dir deren Community-ID bekannt sein. Diese sieht z. B. aus wie <i>+example:matrix.org</i>.",
"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?",
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n": "<h1>HTML für deine Community-Seite</h1>\n<p>\n Nutze die lange Beschreibung um neuen Mitgliedern diese Community zu beschreiben\n oder um einige wichtige Informationen oder <a href=\"foo\">Links</a> festzuhalten.\n</p>\n<p>\n Du kannst auch 'img'-Tags (HTML) verwenden\n</p>\n",
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />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.<br />Klicke hier um die Einstellungen zu öffnen und ihr eine zu geben!"
}

View file

@ -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 <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.",
"You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.",
"You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> 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.<br />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.<br />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.",
"<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.": "<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
"<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> 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 <a>invite others</a> or <a>stop warning about the empty room</a>?": "There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
"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",
"<not supported>": "<not supported>",
"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 <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
"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:",

View file

@ -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"
}

View file

@ -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é linvitation 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 <a>web browser</a>": "La vérification robot n'est pas encore disponible pour le bureau - veuillez utiliser un <a>navigateur</a>"
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "La vérification robot n'est pas encore disponible pour le bureau - veuillez utiliser un <a>navigateur</a>",
"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 <eventType/>, you must be a": "Pour envoyer des évènements du type <eventType/>, 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 <i>+example:matrix.org</i>.": "Pour rejoindre une communauté existante, vous devrez connaître son identifiant. Cela ressemblera à <i>+exemple:matrix.org</i>.",
"There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?": "Il n'y a personne d'autre ici ! Voulez-vous <a>inviter d'autres personnes</a> ou <a>ne plus être notifié de ce salon vide</a> ?",
"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.",
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n": "<h1>HTML pour votre page de communauté</h1>\n<p>\n Utilisez la description longue pour présenter la communauté aux nouveaux membres\n ou pour diffuser des <a href=\"foo\">liens</a> importants\n</p>\n<p>\n Vous pouvez même utiliser des balises \"img\"\n</p>\n",
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />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é.<br />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 ?"
}

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