Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into develop
This commit is contained in:
commit
802abe7091
140 changed files with 7437 additions and 2134 deletions
|
@ -29,10 +29,16 @@ module.exports = {
|
||||||
// so we replace it with a version that is class property aware
|
// so we replace it with a version that is class property aware
|
||||||
"babel/no-invalid-this": "error",
|
"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 **/
|
/** react **/
|
||||||
// This just uses the react plugin to help eslint known when
|
// This just uses the react plugin to help eslint known when
|
||||||
// variables have been used in JSX
|
// variables have been used in JSX
|
||||||
"react/jsx-uses-vars": "error",
|
"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
|
// bind or arrow function in props causes performance issues
|
||||||
"react/jsx-no-bind": ["error", {
|
"react/jsx-no-bind": ["error", {
|
||||||
|
|
284
CHANGELOG.md
284
CHANGELOG.md
|
@ -1,3 +1,287 @@
|
||||||
|
Changes in [0.11.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.3) (2017-12-04)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.2...v0.11.3)
|
||||||
|
|
||||||
|
* Bump js-sdk version to pull in fix for [setting room publicity in a group](https://github.com/matrix-org/matrix-js-sdk/commit/aa3201ebb0fff5af2fb733080aa65ed1f7213de6).
|
||||||
|
|
||||||
|
Changes in [0.11.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.2) (2017-11-28)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.1...v0.11.2)
|
||||||
|
|
||||||
|
* Ignore unrecognised login flows
|
||||||
|
[\#1633](https://github.com/matrix-org/matrix-react-sdk/pull/1633)
|
||||||
|
|
||||||
|
Changes in [0.11.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.1) (2017-11-17)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0...v0.11.1)
|
||||||
|
|
||||||
|
* Fix the force TURN option
|
||||||
|
[\#1621](https://github.com/matrix-org/matrix-react-sdk/pull/1621)
|
||||||
|
|
||||||
|
Changes in [0.11.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0) (2017-11-15)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.3...v0.11.0)
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [0.11.0-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.3) (2017-11-14)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.2...v0.11.0-rc.3)
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [0.11.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.2) (2017-11-10)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.1...v0.11.0-rc.2)
|
||||||
|
|
||||||
|
* Make groups a fully-fleged baked-in feature
|
||||||
|
[\#1603](https://github.com/matrix-org/matrix-react-sdk/pull/1603)
|
||||||
|
|
||||||
|
Changes in [0.11.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.1) (2017-11-10)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7...v0.11.0-rc.1)
|
||||||
|
|
||||||
|
* Improve widget rendering on prop updates
|
||||||
|
[\#1548](https://github.com/matrix-org/matrix-react-sdk/pull/1548)
|
||||||
|
* Display group member profile (avatar/displayname) in ConfirmUserActionDialog
|
||||||
|
[\#1595](https://github.com/matrix-org/matrix-react-sdk/pull/1595)
|
||||||
|
* Don't crash if there isn't a room notif rule
|
||||||
|
[\#1602](https://github.com/matrix-org/matrix-react-sdk/pull/1602)
|
||||||
|
* Show group name in flair tooltip if one is set
|
||||||
|
[\#1596](https://github.com/matrix-org/matrix-react-sdk/pull/1596)
|
||||||
|
* Convert group avatar URL to HTTP before handing to BaseAvatar
|
||||||
|
[\#1597](https://github.com/matrix-org/matrix-react-sdk/pull/1597)
|
||||||
|
* Add group features as starting points for ILAG
|
||||||
|
[\#1601](https://github.com/matrix-org/matrix-react-sdk/pull/1601)
|
||||||
|
* Modify the group room visibility API to reflect the js-sdk changes
|
||||||
|
[\#1598](https://github.com/matrix-org/matrix-react-sdk/pull/1598)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1599](https://github.com/matrix-org/matrix-react-sdk/pull/1599)
|
||||||
|
* Revert "UnknownDeviceDialog: get devices from SDK"
|
||||||
|
[\#1594](https://github.com/matrix-org/matrix-react-sdk/pull/1594)
|
||||||
|
* Order users in the group member list with admins first
|
||||||
|
[\#1591](https://github.com/matrix-org/matrix-react-sdk/pull/1591)
|
||||||
|
* Fetch group members after accepting an invite
|
||||||
|
[\#1592](https://github.com/matrix-org/matrix-react-sdk/pull/1592)
|
||||||
|
* Improve address picker for rooms
|
||||||
|
[\#1589](https://github.com/matrix-org/matrix-react-sdk/pull/1589)
|
||||||
|
* Fix FlairStore getPublicisedGroupsCached to give the correct, existing
|
||||||
|
promise
|
||||||
|
[\#1590](https://github.com/matrix-org/matrix-react-sdk/pull/1590)
|
||||||
|
* Use the getProfileInfo API for group inviter profile
|
||||||
|
[\#1585](https://github.com/matrix-org/matrix-react-sdk/pull/1585)
|
||||||
|
* Add checkbox to GroupAddressPicker for determining visibility of group rooms
|
||||||
|
[\#1587](https://github.com/matrix-org/matrix-react-sdk/pull/1587)
|
||||||
|
* Alter group member api
|
||||||
|
[\#1581](https://github.com/matrix-org/matrix-react-sdk/pull/1581)
|
||||||
|
* Improve group creation UX
|
||||||
|
[\#1580](https://github.com/matrix-org/matrix-react-sdk/pull/1580)
|
||||||
|
* Disable RoomDetailList in GroupView when editing
|
||||||
|
[\#1583](https://github.com/matrix-org/matrix-react-sdk/pull/1583)
|
||||||
|
* Default to no read pins if there is no applicable account data
|
||||||
|
[\#1586](https://github.com/matrix-org/matrix-react-sdk/pull/1586)
|
||||||
|
* UnknownDeviceDialog: get devices from SDK
|
||||||
|
[\#1584](https://github.com/matrix-org/matrix-react-sdk/pull/1584)
|
||||||
|
* Add a small indicator for when a new event is pinned
|
||||||
|
[\#1486](https://github.com/matrix-org/matrix-react-sdk/pull/1486)
|
||||||
|
* Implement tooltip for group rooms
|
||||||
|
[\#1582](https://github.com/matrix-org/matrix-react-sdk/pull/1582)
|
||||||
|
* Room notifs in autocomplete & composer
|
||||||
|
[\#1577](https://github.com/matrix-org/matrix-react-sdk/pull/1577)
|
||||||
|
* Ignore img tags in HTML if src is not specified
|
||||||
|
[\#1579](https://github.com/matrix-org/matrix-react-sdk/pull/1579)
|
||||||
|
* Indicate admins in the group member list with a sheriff badge
|
||||||
|
[\#1578](https://github.com/matrix-org/matrix-react-sdk/pull/1578)
|
||||||
|
* Remember whether widget drawer was hidden per-room
|
||||||
|
[\#1533](https://github.com/matrix-org/matrix-react-sdk/pull/1533)
|
||||||
|
* Throw an error when trying to create a group store with falsey groupId
|
||||||
|
[\#1576](https://github.com/matrix-org/matrix-react-sdk/pull/1576)
|
||||||
|
* Fixes React warning
|
||||||
|
[\#1571](https://github.com/matrix-org/matrix-react-sdk/pull/1571)
|
||||||
|
* Fix Flair not appearing due to missing this._usersInFlight
|
||||||
|
[\#1575](https://github.com/matrix-org/matrix-react-sdk/pull/1575)
|
||||||
|
* Use, if possible, a room's canonical or first alias when viewing the …
|
||||||
|
[\#1574](https://github.com/matrix-org/matrix-react-sdk/pull/1574)
|
||||||
|
* Add CSS classes to group ID input in CreateGroupDialog
|
||||||
|
[\#1573](https://github.com/matrix-org/matrix-react-sdk/pull/1573)
|
||||||
|
* Give autocomplete providers the room they're in
|
||||||
|
[\#1568](https://github.com/matrix-org/matrix-react-sdk/pull/1568)
|
||||||
|
* Fix multiple pills on one line
|
||||||
|
[\#1572](https://github.com/matrix-org/matrix-react-sdk/pull/1572)
|
||||||
|
* Fix group invites such that they look similar to room invites
|
||||||
|
[\#1570](https://github.com/matrix-org/matrix-react-sdk/pull/1570)
|
||||||
|
* Add a GeminiScrollbar to Your Communities
|
||||||
|
[\#1569](https://github.com/matrix-org/matrix-react-sdk/pull/1569)
|
||||||
|
* Fix multiple requests for publicised groups of given user
|
||||||
|
[\#1567](https://github.com/matrix-org/matrix-react-sdk/pull/1567)
|
||||||
|
* Add toggle to alter visibility of a room-group association
|
||||||
|
[\#1566](https://github.com/matrix-org/matrix-react-sdk/pull/1566)
|
||||||
|
* Pillify room notifs in the timeline
|
||||||
|
[\#1564](https://github.com/matrix-org/matrix-react-sdk/pull/1564)
|
||||||
|
* Implement simple GroupRoomInfo
|
||||||
|
[\#1563](https://github.com/matrix-org/matrix-react-sdk/pull/1563)
|
||||||
|
* turn NPE on flair resolution errors into a logged error
|
||||||
|
[\#1565](https://github.com/matrix-org/matrix-react-sdk/pull/1565)
|
||||||
|
* Less translation in parts
|
||||||
|
[\#1484](https://github.com/matrix-org/matrix-react-sdk/pull/1484)
|
||||||
|
* Redact group IDs from analytics
|
||||||
|
[\#1562](https://github.com/matrix-org/matrix-react-sdk/pull/1562)
|
||||||
|
* Display whether the group summary/room list is loading
|
||||||
|
[\#1560](https://github.com/matrix-org/matrix-react-sdk/pull/1560)
|
||||||
|
* Change client-side validation of group IDs to match synapse
|
||||||
|
[\#1558](https://github.com/matrix-org/matrix-react-sdk/pull/1558)
|
||||||
|
* Prevent non-members from opening group settings
|
||||||
|
[\#1559](https://github.com/matrix-org/matrix-react-sdk/pull/1559)
|
||||||
|
* Alter UI for disinviting a group member
|
||||||
|
[\#1556](https://github.com/matrix-org/matrix-react-sdk/pull/1556)
|
||||||
|
* Only show admin tools to privileged users
|
||||||
|
[\#1555](https://github.com/matrix-org/matrix-react-sdk/pull/1555)
|
||||||
|
* Try lowercase username on login
|
||||||
|
[\#1550](https://github.com/matrix-org/matrix-react-sdk/pull/1550)
|
||||||
|
* Don't refresh page on password change prompt
|
||||||
|
[\#1554](https://github.com/matrix-org/matrix-react-sdk/pull/1554)
|
||||||
|
* Fix initial in GroupAvatar in GroupView
|
||||||
|
[\#1553](https://github.com/matrix-org/matrix-react-sdk/pull/1553)
|
||||||
|
* Use "crop" method to scale group avatars in MyGroups
|
||||||
|
[\#1549](https://github.com/matrix-org/matrix-react-sdk/pull/1549)
|
||||||
|
* Lowercase all usernames
|
||||||
|
[\#1547](https://github.com/matrix-org/matrix-react-sdk/pull/1547)
|
||||||
|
* Add sensible missing entry generator for MELS tests
|
||||||
|
[\#1546](https://github.com/matrix-org/matrix-react-sdk/pull/1546)
|
||||||
|
* Fix prompt to re-use chat room
|
||||||
|
[\#1545](https://github.com/matrix-org/matrix-react-sdk/pull/1545)
|
||||||
|
* Add unregiseterListener to GroupStore
|
||||||
|
[\#1544](https://github.com/matrix-org/matrix-react-sdk/pull/1544)
|
||||||
|
* Fix groups invited users err for non members
|
||||||
|
[\#1543](https://github.com/matrix-org/matrix-react-sdk/pull/1543)
|
||||||
|
* Add Mention button to MemberInfo
|
||||||
|
[\#1532](https://github.com/matrix-org/matrix-react-sdk/pull/1532)
|
||||||
|
* Only show group settings cog to members
|
||||||
|
[\#1541](https://github.com/matrix-org/matrix-react-sdk/pull/1541)
|
||||||
|
* Use correct icon for group room deletion and make themeable
|
||||||
|
[\#1540](https://github.com/matrix-org/matrix-react-sdk/pull/1540)
|
||||||
|
* Add invite button to MemberInfo if user has left or wasn't in room
|
||||||
|
[\#1534](https://github.com/matrix-org/matrix-react-sdk/pull/1534)
|
||||||
|
* Add option to mirror local video feed
|
||||||
|
[\#1539](https://github.com/matrix-org/matrix-react-sdk/pull/1539)
|
||||||
|
* Use the correct userId when displaying who redacted a message
|
||||||
|
[\#1538](https://github.com/matrix-org/matrix-react-sdk/pull/1538)
|
||||||
|
* Only show editing UI for aliases/related_groups for users /w power
|
||||||
|
[\#1529](https://github.com/matrix-org/matrix-react-sdk/pull/1529)
|
||||||
|
* Swap from `ui_opacity` to `panel_disabled`
|
||||||
|
[\#1535](https://github.com/matrix-org/matrix-react-sdk/pull/1535)
|
||||||
|
* Fix room address picker tiles default name
|
||||||
|
[\#1536](https://github.com/matrix-org/matrix-react-sdk/pull/1536)
|
||||||
|
* T3chguy/hide level change on 50
|
||||||
|
[\#1531](https://github.com/matrix-org/matrix-react-sdk/pull/1531)
|
||||||
|
* fix missing date sep caused by hidden event at start of day
|
||||||
|
[\#1537](https://github.com/matrix-org/matrix-react-sdk/pull/1537)
|
||||||
|
* Add a delete confirmation dialog for widgets
|
||||||
|
[\#1520](https://github.com/matrix-org/matrix-react-sdk/pull/1520)
|
||||||
|
* When dispatching view_[my_]group[s], reset RoomViewStore
|
||||||
|
[\#1530](https://github.com/matrix-org/matrix-react-sdk/pull/1530)
|
||||||
|
* Prevent editing of UI requiring user privilege if user unprivileged
|
||||||
|
[\#1528](https://github.com/matrix-org/matrix-react-sdk/pull/1528)
|
||||||
|
* Use the correct property of the API room objects
|
||||||
|
[\#1526](https://github.com/matrix-org/matrix-react-sdk/pull/1526)
|
||||||
|
* Don't include the |other in the translation value
|
||||||
|
[\#1527](https://github.com/matrix-org/matrix-react-sdk/pull/1527)
|
||||||
|
* Re-run gen-i18n after fixing https://github.com/matrix-org/matrix-react-
|
||||||
|
sdk/pull/1521
|
||||||
|
[\#1525](https://github.com/matrix-org/matrix-react-sdk/pull/1525)
|
||||||
|
* Fix some react warnings in GroupMemberList
|
||||||
|
[\#1522](https://github.com/matrix-org/matrix-react-sdk/pull/1522)
|
||||||
|
* Fix bug with gen-i18n/js when adding new plurals
|
||||||
|
[\#1521](https://github.com/matrix-org/matrix-react-sdk/pull/1521)
|
||||||
|
* Make GroupStoreCache global for cross-package access
|
||||||
|
[\#1524](https://github.com/matrix-org/matrix-react-sdk/pull/1524)
|
||||||
|
* Add fields needed by RoomDetailList to groupRoomFromApiObject
|
||||||
|
[\#1523](https://github.com/matrix-org/matrix-react-sdk/pull/1523)
|
||||||
|
* Only show flair for groups with avatars set
|
||||||
|
[\#1519](https://github.com/matrix-org/matrix-react-sdk/pull/1519)
|
||||||
|
* Refresh group member lists after inviting users
|
||||||
|
[\#1518](https://github.com/matrix-org/matrix-react-sdk/pull/1518)
|
||||||
|
* Invalidate the user's public groups cache when changing group publicity
|
||||||
|
[\#1517](https://github.com/matrix-org/matrix-react-sdk/pull/1517)
|
||||||
|
* Make the gen-i18n script validate _t calls
|
||||||
|
[\#1515](https://github.com/matrix-org/matrix-react-sdk/pull/1515)
|
||||||
|
* Add placeholder to MyGroups page, adjust CSS classes
|
||||||
|
[\#1514](https://github.com/matrix-org/matrix-react-sdk/pull/1514)
|
||||||
|
* Rxl881/parallelshell
|
||||||
|
[\#1338](https://github.com/matrix-org/matrix-react-sdk/pull/1338)
|
||||||
|
* Run prunei18n
|
||||||
|
[\#1513](https://github.com/matrix-org/matrix-react-sdk/pull/1513)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1512](https://github.com/matrix-org/matrix-react-sdk/pull/1512)
|
||||||
|
* Add script to prune unused translations
|
||||||
|
[\#1502](https://github.com/matrix-org/matrix-react-sdk/pull/1502)
|
||||||
|
* Fix creation of DM rooms
|
||||||
|
[\#1510](https://github.com/matrix-org/matrix-react-sdk/pull/1510)
|
||||||
|
* Group create dialog: only enter localpart
|
||||||
|
[\#1507](https://github.com/matrix-org/matrix-react-sdk/pull/1507)
|
||||||
|
* Improve MyGroups UI
|
||||||
|
[\#1509](https://github.com/matrix-org/matrix-react-sdk/pull/1509)
|
||||||
|
* Use object URLs to load Files in to images
|
||||||
|
[\#1508](https://github.com/matrix-org/matrix-react-sdk/pull/1508)
|
||||||
|
* Add clientside error for non-alphanumeric group ID
|
||||||
|
[\#1506](https://github.com/matrix-org/matrix-react-sdk/pull/1506)
|
||||||
|
* Fix invites to groups without names
|
||||||
|
[\#1505](https://github.com/matrix-org/matrix-react-sdk/pull/1505)
|
||||||
|
* Add warning when adding group rooms/users
|
||||||
|
[\#1504](https://github.com/matrix-org/matrix-react-sdk/pull/1504)
|
||||||
|
* More Groups->Communities
|
||||||
|
[\#1503](https://github.com/matrix-org/matrix-react-sdk/pull/1503)
|
||||||
|
* Groups -> Communities
|
||||||
|
[\#1501](https://github.com/matrix-org/matrix-react-sdk/pull/1501)
|
||||||
|
* Factor out Flair cache into FlairStore
|
||||||
|
[\#1500](https://github.com/matrix-org/matrix-react-sdk/pull/1500)
|
||||||
|
* Add i18n script to package.json
|
||||||
|
[\#1499](https://github.com/matrix-org/matrix-react-sdk/pull/1499)
|
||||||
|
* Make gen-i18n support 'HTML'
|
||||||
|
[\#1498](https://github.com/matrix-org/matrix-react-sdk/pull/1498)
|
||||||
|
* fix editing visuals on groupview header
|
||||||
|
[\#1497](https://github.com/matrix-org/matrix-react-sdk/pull/1497)
|
||||||
|
* Script to generate the translations base file
|
||||||
|
[\#1493](https://github.com/matrix-org/matrix-react-sdk/pull/1493)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1495](https://github.com/matrix-org/matrix-react-sdk/pull/1495)
|
||||||
|
* Attempt to relate a group to a room when adding it
|
||||||
|
[\#1494](https://github.com/matrix-org/matrix-react-sdk/pull/1494)
|
||||||
|
* Shuffle GroupView UI
|
||||||
|
[\#1490](https://github.com/matrix-org/matrix-react-sdk/pull/1490)
|
||||||
|
* Fix bug preventing partial group profile
|
||||||
|
[\#1491](https://github.com/matrix-org/matrix-react-sdk/pull/1491)
|
||||||
|
* Don't show room IDs when picking rooms
|
||||||
|
[\#1492](https://github.com/matrix-org/matrix-react-sdk/pull/1492)
|
||||||
|
* Only show invited section if there are invited group members
|
||||||
|
[\#1489](https://github.com/matrix-org/matrix-react-sdk/pull/1489)
|
||||||
|
* Show "Invited" section in the user list
|
||||||
|
[\#1488](https://github.com/matrix-org/matrix-react-sdk/pull/1488)
|
||||||
|
* Refactor class names for an entity tile being hovered over
|
||||||
|
[\#1487](https://github.com/matrix-org/matrix-react-sdk/pull/1487)
|
||||||
|
* Modify GroupView UI
|
||||||
|
[\#1475](https://github.com/matrix-org/matrix-react-sdk/pull/1475)
|
||||||
|
* Message/event pinning
|
||||||
|
[\#1439](https://github.com/matrix-org/matrix-react-sdk/pull/1439)
|
||||||
|
* Remove duplicate declaration that breaks the build
|
||||||
|
[\#1483](https://github.com/matrix-org/matrix-react-sdk/pull/1483)
|
||||||
|
* Include magnet scheme in sanitize HTML params
|
||||||
|
[\#1301](https://github.com/matrix-org/matrix-react-sdk/pull/1301)
|
||||||
|
* Add a way to jump to a user's Read Receipt from MemberInfo
|
||||||
|
[\#1454](https://github.com/matrix-org/matrix-react-sdk/pull/1454)
|
||||||
|
* Use standard subsitution syntax in _tJsx
|
||||||
|
[\#1462](https://github.com/matrix-org/matrix-react-sdk/pull/1462)
|
||||||
|
* Don't suggest grey as a color scheme for a room
|
||||||
|
[\#1442](https://github.com/matrix-org/matrix-react-sdk/pull/1442)
|
||||||
|
* allow hiding of notification body for privacy reasons
|
||||||
|
[\#1362](https://github.com/matrix-org/matrix-react-sdk/pull/1362)
|
||||||
|
* Suggest to invite people when speaking in an empty room
|
||||||
|
[\#1466](https://github.com/matrix-org/matrix-react-sdk/pull/1466)
|
||||||
|
* Buttons to remove room/self avatar
|
||||||
|
[\#1478](https://github.com/matrix-org/matrix-react-sdk/pull/1478)
|
||||||
|
* T3chguy/fix memberlist
|
||||||
|
[\#1480](https://github.com/matrix-org/matrix-react-sdk/pull/1480)
|
||||||
|
* add option to disable BigEmoji
|
||||||
|
[\#1481](https://github.com/matrix-org/matrix-react-sdk/pull/1481)
|
||||||
|
|
||||||
Changes in [0.10.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7) (2017-10-16)
|
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)
|
[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
151
docs/settings.md
Normal 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"`.
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.10.7",
|
"version": "0.11.3",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -71,9 +71,10 @@
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.3",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"matrix-js-sdk": "0.8.5",
|
"matrix-js-sdk": "0.9.2",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
|
"querystring": "^0.2.0",
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
"react-addons-css-transition-group": "15.3.2",
|
"react-addons-css-transition-group": "15.3.2",
|
||||||
"react-dom": "^15.4.0",
|
"react-dom": "^15.4.0",
|
||||||
|
|
|
@ -32,7 +32,7 @@ const walk = require('walk');
|
||||||
const flowParser = require('flow-parser');
|
const flowParser = require('flow-parser');
|
||||||
const estreeWalker = require('estree-walker');
|
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 INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json';
|
||||||
const OUTPUT_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;
|
if (tKey === null) return;
|
||||||
|
|
||||||
// check the format string against the args
|
// 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') {
|
if (node.callee.name === '_t') {
|
||||||
try {
|
try {
|
||||||
const placeholders = getFormatStrings(tKey);
|
const placeholders = getFormatStrings(tKey);
|
||||||
|
@ -139,6 +139,22 @@ function getTranslationsJs(file) {
|
||||||
throw new Error(`No value found for placeholder '${placeholder}'`);
|
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) {
|
} catch (e) {
|
||||||
console.log();
|
console.log();
|
||||||
console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`);
|
console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`);
|
||||||
|
|
|
@ -52,7 +52,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import UserSettingsStore from './UserSettingsStore';
|
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
|
@ -245,9 +244,7 @@ function _onAction(payload) {
|
||||||
return;
|
return;
|
||||||
} else if (members.length === 2) {
|
} else if (members.length === 2) {
|
||||||
console.log("Place %s call in %s", payload.type, payload.room_id);
|
console.log("Place %s call in %s", payload.type, payload.room_id);
|
||||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
|
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
|
||||||
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
|
|
||||||
});
|
|
||||||
placeCall(call);
|
placeCall(call);
|
||||||
} else { // > 2
|
} else { // > 2
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import UserSettingsStore from './UserSettingsStore';
|
|
||||||
import * as Matrix from 'matrix-js-sdk';
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
|
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getDevices: function() {
|
getDevices: function() {
|
||||||
|
@ -43,22 +43,20 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
loadDevices: function() {
|
loadDevices: function() {
|
||||||
// this.getDevices().then((devices) => {
|
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||||
const localSettings = UserSettingsStore.getLocalSettings();
|
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||||
// // if deviceId is not found, automatic fallback is in spec
|
|
||||||
// // recall previously stored inputs if any
|
Matrix.setMatrixCallAudioInput(audioDeviceId);
|
||||||
Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']);
|
Matrix.setMatrixCallVideoInput(videoDeviceId);
|
||||||
Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']);
|
|
||||||
// });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setAudioInput: function(deviceId) {
|
setAudioInput: function(deviceId) {
|
||||||
UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId);
|
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
Matrix.setMatrixCallAudioInput(deviceId);
|
Matrix.setMatrixCallAudioInput(deviceId);
|
||||||
},
|
},
|
||||||
|
|
||||||
setVideoInput: function(deviceId) {
|
setVideoInput: function(deviceId) {
|
||||||
UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId);
|
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
Matrix.setMatrixCallVideoInput(deviceId);
|
Matrix.setMatrixCallVideoInput(deviceId);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -49,20 +49,26 @@ export function showGroupInviteDialog(groupId) {
|
||||||
|
|
||||||
export function showGroupAddRoomDialog(groupId) {
|
export function showGroupAddRoomDialog(groupId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
let addRoomsPublicly = false;
|
||||||
|
const onCheckboxClicked = (e) => {
|
||||||
|
addRoomsPublicly = e.target.checked;
|
||||||
|
};
|
||||||
const description = <div>
|
const description = <div>
|
||||||
<div>{ _t("Which rooms would you like to add to this community?") }</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>;
|
</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");
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
|
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
|
||||||
title: _t("Add rooms to the community"),
|
title: _t("Add rooms to the community"),
|
||||||
description: description,
|
description: description,
|
||||||
|
extraNode: checkboxContainer,
|
||||||
placeholder: _t("Room name or alias"),
|
placeholder: _t("Room name or alias"),
|
||||||
button: _t("Add to community"),
|
button: _t("Add to community"),
|
||||||
pickerType: 'room',
|
pickerType: 'room',
|
||||||
|
@ -70,7 +76,7 @@ export function showGroupAddRoomDialog(groupId) {
|
||||||
onFinished: (success, addrs) => {
|
onFinished: (success, addrs) => {
|
||||||
if (!success) return;
|
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 matrixClient = MatrixClientPeg.get();
|
||||||
const groupStore = GroupStoreCache.getGroupStore(matrixClient, groupId);
|
const groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
return Promise.all(addrs.map((addr) => {
|
return Promise.all(addrs.map((addr) => {
|
||||||
return groupStore
|
return groupStore
|
||||||
.addRoomToGroup(addr.address)
|
.addRoomToGroup(addr.address, addRoomsPublicly)
|
||||||
.catch(() => { errorList.push(addr.address); })
|
.catch(() => { errorList.push(addr.address); })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const roomId = addr.address;
|
const roomId = addr.address;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 */
|
/* a selection of key codes, as used in KeyboardEvent.keyCode */
|
||||||
module.exports = {
|
export const KeyCode = {
|
||||||
BACKSPACE: 8,
|
BACKSPACE: 8,
|
||||||
TAB: 9,
|
TAB: 9,
|
||||||
ENTER: 13,
|
ENTER: 13,
|
||||||
|
@ -58,3 +59,12 @@ module.exports = {
|
||||||
KEY_Y: 89,
|
KEY_Y: 89,
|
||||||
KEY_Z: 90,
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -389,6 +389,8 @@ function _persistCredentialsToLocalStorage(credentials) {
|
||||||
* Logs the current session out and transitions to the logged-out state
|
* Logs the current session out and transitions to the logged-out state
|
||||||
*/
|
*/
|
||||||
export function logout() {
|
export function logout() {
|
||||||
|
if (!MatrixClientPeg.get()) return;
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
// logout doesn't work for guest sessions
|
// logout doesn't work for guest sessions
|
||||||
// Also we sometimes want to re-log in a guest session
|
// Also we sometimes want to re-log in a guest session
|
||||||
|
@ -436,6 +438,10 @@ function startMatrixClient() {
|
||||||
DMRoomMap.makeShared().start();
|
DMRoomMap.makeShared().start();
|
||||||
|
|
||||||
MatrixClientPeg.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'});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -204,6 +204,12 @@ export default class Login {
|
||||||
}
|
}
|
||||||
throw originalLoginError;
|
throw originalLoginError;
|
||||||
}).catch((error) => {
|
}).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 (
|
if (
|
||||||
error.httpStatus === 403 &&
|
error.httpStatus === 403 &&
|
||||||
loginParams.identifier.type === 'm.id.user' &&
|
loginParams.identifier.type === 'm.id.user' &&
|
||||||
|
@ -211,6 +217,7 @@ export default class Login {
|
||||||
) {
|
) {
|
||||||
return tryLowercaseUsername(originalLoginError);
|
return tryLowercaseUsername(originalLoginError);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
throw originalLoginError;
|
throw originalLoginError;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.log("Login failed", error);
|
console.log("Login failed", error);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import utils from 'matrix-js-sdk/lib/utils';
|
||||||
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
|
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
|
||||||
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
|
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
|
||||||
import createMatrixClient from './utils/createMatrixClient';
|
import createMatrixClient from './utils/createMatrixClient';
|
||||||
|
import SettingsStore from './settings/SettingsStore';
|
||||||
|
|
||||||
interface MatrixClientCreds {
|
interface MatrixClientCreds {
|
||||||
homeserverUrl: string,
|
homeserverUrl: string,
|
||||||
|
@ -93,6 +94,7 @@ class MatrixClientPeg {
|
||||||
const opts = utils.deepCopy(this.opts);
|
const opts = utils.deepCopy(this.opts);
|
||||||
// the react sdk doesn't work without this, so don't allow
|
// the react sdk doesn't work without this, so don't allow
|
||||||
opts.pendingEventOrdering = "detached";
|
opts.pendingEventOrdering = "detached";
|
||||||
|
opts.disablePresence = true; // we do this manually
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const promise = this.matrixClient.store.startup();
|
const promise = this.matrixClient.store.startup();
|
||||||
|
@ -143,6 +145,7 @@ class MatrixClientPeg {
|
||||||
userId: creds.userId,
|
userId: creds.userId,
|
||||||
deviceId: creds.deviceId,
|
deviceId: creds.deviceId,
|
||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
|
forceTURN: SettingsStore.getValue('webRtcForceTURN', false),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript);
|
this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript);
|
||||||
|
|
|
@ -25,6 +25,7 @@ import dis from './dispatcher';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Dispatches:
|
* Dispatches:
|
||||||
|
@ -138,10 +139,8 @@ const Notifier = {
|
||||||
|
|
||||||
// make sure that we persist the current setting audio_enabled setting
|
// make sure that we persist the current setting audio_enabled setting
|
||||||
// before changing anything
|
// before changing anything
|
||||||
if (global.localStorage) {
|
if (SettingsStore.isLevelSupported(SettingLevel.DEVICE)) {
|
||||||
if (global.localStorage.getItem('audio_notifications_enabled') === null) {
|
SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, this.isEnabled());
|
||||||
this.setAudioEnabled(this.isEnabled());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
|
@ -149,6 +148,7 @@ const Notifier = {
|
||||||
plaf.requestNotificationPermission().done((result) => {
|
plaf.requestNotificationPermission().done((result) => {
|
||||||
if (result !== 'granted') {
|
if (result !== 'granted') {
|
||||||
// The permission request was dismissed or denied
|
// The permission request was dismissed or denied
|
||||||
|
// TODO: Support alternative branding in messaging
|
||||||
const description = result === 'denied'
|
const description = result === 'denied'
|
||||||
? _t('Riot does not have permission to send you notifications - please check your browser settings')
|
? _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');
|
: _t('Riot was not given permission to send notifications - please try again');
|
||||||
|
@ -160,10 +160,6 @@ const Notifier = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (global.localStorage) {
|
|
||||||
global.localStorage.setItem('notifications_enabled', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
|
@ -174,8 +170,6 @@ const Notifier = {
|
||||||
// disabled again in the future, we will show the banner again.
|
// disabled again in the future, we will show the banner again.
|
||||||
this.setToolbarHidden(false);
|
this.setToolbarHidden(false);
|
||||||
} else {
|
} else {
|
||||||
if (!global.localStorage) return;
|
|
||||||
global.localStorage.setItem('notifications_enabled', 'false');
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
value: false,
|
value: false,
|
||||||
|
@ -184,44 +178,24 @@ const Notifier = {
|
||||||
},
|
},
|
||||||
|
|
||||||
isEnabled: function() {
|
isEnabled: function() {
|
||||||
|
return this.isPossible() && SettingsStore.getValue("notificationsEnabled");
|
||||||
|
},
|
||||||
|
|
||||||
|
isPossible: function() {
|
||||||
const plaf = PlatformPeg.get();
|
const plaf = PlatformPeg.get();
|
||||||
if (!plaf) return false;
|
if (!plaf) return false;
|
||||||
if (!plaf.supportsNotifications()) return false;
|
if (!plaf.supportsNotifications()) return false;
|
||||||
if (!plaf.maySendNotifications()) return false;
|
if (!plaf.maySendNotifications()) return false;
|
||||||
|
|
||||||
if (!global.localStorage) return true;
|
return true; // possible, but not necessarily enabled
|
||||||
|
|
||||||
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');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
isBodyEnabled: function() {
|
isBodyEnabled: function() {
|
||||||
if (!global.localStorage) return true;
|
return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled");
|
||||||
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';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setAudioEnabled: function(enable) {
|
isAudioEnabled: function() {
|
||||||
if (!global.localStorage) return;
|
return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled");
|
||||||
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';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setToolbarHidden: function(hidden, persistent = true) {
|
setToolbarHidden: function(hidden, persistent = true) {
|
||||||
|
@ -238,16 +212,14 @@ const Notifier = {
|
||||||
|
|
||||||
// update the info to localStorage for persistent settings
|
// update the info to localStorage for persistent settings
|
||||||
if (persistent && global.localStorage) {
|
if (persistent && global.localStorage) {
|
||||||
global.localStorage.setItem('notifications_hidden', hidden);
|
global.localStorage.setItem("notifications_hidden", hidden);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
isToolbarHidden: function() {
|
isToolbarHidden: function() {
|
||||||
// Check localStorage for any such meta data
|
// Check localStorage for any such meta data
|
||||||
if (global.localStorage) {
|
if (global.localStorage) {
|
||||||
if (global.localStorage.getItem('notifications_hidden') === 'true') {
|
return global.localStorage.getItem("notifications_hidden") === "true";
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.toolbarHidden;
|
return this.toolbarHidden;
|
||||||
|
|
|
@ -56,13 +56,27 @@ class Presence {
|
||||||
return this.state;
|
return this.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current status message.
|
||||||
|
* @returns {String} the status message, may be null
|
||||||
|
*/
|
||||||
|
getStatusMessage() {
|
||||||
|
return this.statusMessage;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the presence state.
|
* Set the presence state.
|
||||||
* If the state has changed, the Home Server will be notified.
|
* If the state has changed, the Home Server will be notified.
|
||||||
* @param {string} newState the new presence state (see PRESENCE enum)
|
* @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) {
|
setState(newState, statusMessage=null, maintain=false) {
|
||||||
if (newState === this.state) {
|
if (this.maintain) {
|
||||||
|
// Don't update presence if we're maintaining a particular status
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newState === this.state && statusMessage === this.statusMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (PRESENCE_STATES.indexOf(newState) === -1) {
|
if (PRESENCE_STATES.indexOf(newState) === -1) {
|
||||||
|
@ -72,21 +86,37 @@ class Presence {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const old_state = this.state;
|
const old_state = this.state;
|
||||||
|
const old_message = this.statusMessage;
|
||||||
this.state = newState;
|
this.state = newState;
|
||||||
|
this.statusMessage = statusMessage;
|
||||||
|
this.maintain = maintain;
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return; // don't try to set presence when a guest; it won't work.
|
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;
|
const self = this;
|
||||||
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
MatrixClientPeg.get().setPresence(updateContent).done(function() {
|
||||||
console.log("Presence: %s", newState);
|
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) {
|
}, function(err) {
|
||||||
console.error("Failed to set presence: %s", err);
|
console.error("Failed to set presence: %s", err);
|
||||||
self.state = old_state;
|
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.
|
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
|
||||||
* @private
|
* @private
|
||||||
|
@ -95,7 +125,8 @@ class Presence {
|
||||||
this.setState("unavailable");
|
this.setState("unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUserActivity() {
|
_onUserActivity(payload) {
|
||||||
|
if (payload.action === "sync_state" || payload.action === "self_presence_updated") return;
|
||||||
this._resetTimer();
|
this._resetTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
11
src/Roles.js
11
src/Roles.js
|
@ -15,19 +15,20 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
export function levelRoleMap() {
|
export function levelRoleMap(usersDefault) {
|
||||||
return {
|
return {
|
||||||
undefined: _t('Default'),
|
undefined: _t('Default'),
|
||||||
0: _t('User'),
|
0: _t('Restricted'),
|
||||||
|
[usersDefault]: _t('Default'),
|
||||||
50: _t('Moderator'),
|
50: _t('Moderator'),
|
||||||
100: _t('Admin'),
|
100: _t('Admin'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function textualPowerLevel(level, userDefault) {
|
export function textualPowerLevel(level, usersDefault) {
|
||||||
const LEVEL_ROLE_MAP = this.levelRoleMap();
|
const LEVEL_ROLE_MAP = this.levelRoleMap(usersDefault);
|
||||||
if (LEVEL_ROLE_MAP[level]) {
|
if (LEVEL_ROLE_MAP[level]) {
|
||||||
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`);
|
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`);
|
||||||
} else {
|
} else {
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ const DEFAULTS = {
|
||||||
class SdkConfig {
|
class SdkConfig {
|
||||||
|
|
||||||
static get() {
|
static get() {
|
||||||
return global.mxReactSdkConfig;
|
return global.mxReactSdkConfig || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
static put(cfg) {
|
static put(cfg) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import Tinter from "./Tinter";
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||||
|
|
||||||
|
|
||||||
class Command {
|
class Command {
|
||||||
|
@ -97,9 +98,7 @@ const commands = {
|
||||||
colorScheme.secondary_color = matches[4];
|
colorScheme.secondary_color = matches[4];
|
||||||
}
|
}
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().setRoomAccountData(
|
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
||||||
roomId, "org.matrix.room.color_scheme", colorScheme,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
446
src/Tinter.js
446
src/Tinter.js
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015 OpenMarket Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
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;
|
const DEBUG = 0;
|
||||||
|
|
||||||
// The colour keys to be replaced as referred to in CSS
|
// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
|
||||||
const keyRgb = [
|
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(118, 207, 166)", // Vector Green
|
||||||
"rgb(234, 245, 240)", // Vector Light 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
|
// 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
|
// x = (255 - 234) / (255 - 118) = 0.16
|
||||||
|
|
||||||
// The colour keys to be replaced as referred to in SVGs
|
// The colour keys to be replaced as referred to in SVGs
|
||||||
const keyHex = [
|
this.keyHex = [
|
||||||
"#76CFA6", // Vector Green
|
"#76CFA6", // Vector Green
|
||||||
"#EAF5F0", // Vector Light 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)
|
"#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.
|
// defaults to our keys.
|
||||||
const colors = [
|
this.colors = [
|
||||||
keyHex[0],
|
this.keyHex[0],
|
||||||
keyHex[1],
|
this.keyHex[1],
|
||||||
keyHex[2],
|
this.keyHex[2],
|
||||||
keyHex[3],
|
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
|
// style: a style object that should be fixed up taken from a stylesheet
|
||||||
// attr: name of the attribute to be clobbered, e.g. 'color'
|
// attr: name of the attribute to be clobbered, e.g. 'color'
|
||||||
// index: ordinal of primary, secondary or tertiary
|
// index: ordinal of primary, secondary or tertiary
|
||||||
|
// },
|
||||||
// }
|
// }
|
||||||
];
|
];
|
||||||
|
|
||||||
// CSS attributes to be fixed up
|
// CSS attributes to be fixed up
|
||||||
const cssAttrs = [
|
this.cssAttrs = [
|
||||||
"color",
|
"color",
|
||||||
"backgroundColor",
|
"backgroundColor",
|
||||||
"borderColor",
|
"borderColor",
|
||||||
|
@ -69,15 +119,184 @@ const cssAttrs = [
|
||||||
"borderLeftColor",
|
"borderLeftColor",
|
||||||
];
|
];
|
||||||
|
|
||||||
const svgAttrs = [
|
this.svgAttrs = [
|
||||||
"fill",
|
"fill",
|
||||||
"stroke",
|
"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++) {
|
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||||
const ss = document.styleSheets[i];
|
const ss = document.styleSheets[i];
|
||||||
if (!ss) continue; // well done safari >:(
|
if (!ss) continue; // well done safari >:(
|
||||||
|
@ -100,18 +319,29 @@ function calcCssFixups() {
|
||||||
// Iterating through the CSS looking for matches to hack on feels
|
// Iterating through the CSS looking for matches to hack on feels
|
||||||
// pretty horrible anyway. And what if the application skin doesn't use
|
// pretty horrible anyway. And what if the application skin doesn't use
|
||||||
// Vector Green as its primary color?
|
// 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 (!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++) {
|
for (let j = 0; j < ss.cssRules.length; j++) {
|
||||||
const rule = ss.cssRules[j];
|
const rule = ss.cssRules[j];
|
||||||
if (!rule.style) continue;
|
if (!rule.style) continue;
|
||||||
for (let k = 0; k < cssAttrs.length; k++) {
|
if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
|
||||||
const attr = cssAttrs[k];
|
for (let k = 0; k < this.cssAttrs.length; k++) {
|
||||||
for (let l = 0; l < keyRgb.length; l++) {
|
const attr = this.cssAttrs[k];
|
||||||
if (rule.style[attr] === keyRgb[l]) {
|
for (let l = 0; l < this.keyRgb.length; l++) {
|
||||||
cssFixups.push({
|
if (rule.style[attr] === this.keyRgb[l]) {
|
||||||
|
this.cssFixups[this.theme].push({
|
||||||
style: rule.style,
|
style: rule.style,
|
||||||
attr: attr,
|
attr: attr,
|
||||||
index: l,
|
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() {
|
applyCssFixups() {
|
||||||
if (DEBUG) console.log("applyCssFixups start");
|
if (DEBUG) {
|
||||||
for (let i = 0; i < cssFixups.length; i++) {
|
console.log("applyCssFixups start (" +
|
||||||
const cssFixup = cssFixups[i];
|
this.cssFixups[this.theme].length +
|
||||||
cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
|
" 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");
|
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
|
// 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)
|
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
|
||||||
// keeping it here for now.
|
// keeping it here for now.
|
||||||
calcSvgFixups: function(svgs) {
|
calcSvgFixups(svgs) {
|
||||||
// go through manually fixing up SVG colours.
|
// go through manually fixing up SVG colours.
|
||||||
// we could do this by stylesheets, but keeping the stylesheets
|
// we could do this by stylesheets, but keeping the stylesheets
|
||||||
// updated would be a PITA, so just brute-force search for the
|
// 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);
|
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
|
||||||
const fixups = [];
|
const fixups = [];
|
||||||
for (let i = 0; i < svgs.length; i++) {
|
for (let i = 0; i < svgs.length; i++) {
|
||||||
var svgDoc;
|
let svgDoc;
|
||||||
try {
|
try {
|
||||||
svgDoc = svgs[i].contentDocument;
|
svgDoc = svgs[i].contentDocument;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -259,16 +401,17 @@ module.exports = {
|
||||||
if (e.stack) {
|
if (e.stack) {
|
||||||
msg += ' | stack: ' + e.stack;
|
msg += ' | stack: ' + e.stack;
|
||||||
}
|
}
|
||||||
console.error(e);
|
console.error(msg);
|
||||||
}
|
}
|
||||||
if (!svgDoc) continue;
|
if (!svgDoc) continue;
|
||||||
const tags = svgDoc.getElementsByTagName("*");
|
const tags = svgDoc.getElementsByTagName("*");
|
||||||
for (let j = 0; j < tags.length; j++) {
|
for (let j = 0; j < tags.length; j++) {
|
||||||
const tag = tags[j];
|
const tag = tags[j];
|
||||||
for (let k = 0; k < svgAttrs.length; k++) {
|
for (let k = 0; k < this.svgAttrs.length; k++) {
|
||||||
const attr = svgAttrs[k];
|
const attr = this.svgAttrs[k];
|
||||||
for (let l = 0; l < keyHex.length; l++) {
|
for (let l = 0; l < this.keyHex.length; l++) {
|
||||||
if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) {
|
if (tag.getAttribute(attr) &&
|
||||||
|
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
|
||||||
fixups.push({
|
fixups.push({
|
||||||
node: tag,
|
node: tag,
|
||||||
attr: attr,
|
attr: attr,
|
||||||
|
@ -282,14 +425,19 @@ module.exports = {
|
||||||
if (DEBUG) console.log("calcSvgFixups end");
|
if (DEBUG) console.log("calcSvgFixups end");
|
||||||
|
|
||||||
return fixups;
|
return fixups;
|
||||||
},
|
}
|
||||||
|
|
||||||
applySvgFixups: function(fixups) {
|
applySvgFixups(fixups) {
|
||||||
if (DEBUG) console.log("applySvgFixups start for " + fixups);
|
if (DEBUG) console.log("applySvgFixups start for " + fixups);
|
||||||
for (let i = 0; i < fixups.length; i++) {
|
for (let i = 0; i < fixups.length; i++) {
|
||||||
const svgFixup = fixups[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 (DEBUG) console.log("applySvgFixups end");
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
if (global.singletonTinter === undefined) {
|
||||||
|
global.singletonTinter = new Tinter();
|
||||||
|
}
|
||||||
|
export default global.singletonTinter;
|
||||||
|
|
|
@ -25,6 +25,7 @@ const onAction = function(payload) {
|
||||||
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
||||||
isDialogOpen = true;
|
isDialogOpen = true;
|
||||||
Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
|
Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
|
||||||
|
devices: payload.err.devices,
|
||||||
room: payload.room,
|
room: payload.room,
|
||||||
onFinished: (r) => {
|
onFinished: (r) => {
|
||||||
isDialogOpen = false;
|
isDialogOpen = false;
|
||||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const MatrixClientPeg = require('./MatrixClientPeg');
|
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
import UserSettingsStore from './UserSettingsStore';
|
|
||||||
import shouldHideEvent from './shouldHideEvent';
|
import shouldHideEvent from './shouldHideEvent';
|
||||||
const sdk = require('./index');
|
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,
|
// we have and the read receipt. We could fetch more history to try & find out,
|
||||||
// but currently we just guess.
|
// but currently we just guess.
|
||||||
|
|
||||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
|
||||||
// Loop through messages, starting with the most recent...
|
// Loop through messages, starting with the most recent...
|
||||||
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
||||||
const ev = room.timeline[i];
|
const ev = room.timeline[i];
|
||||||
|
@ -74,7 +72,7 @@ module.exports = {
|
||||||
// that counts and we can stop looking because the user's read
|
// that counts and we can stop looking because the user's read
|
||||||
// this and everything before.
|
// this and everything before.
|
||||||
return false;
|
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
|
// We've found a message that counts before we hit
|
||||||
// the read marker, so this room is definitely unread.
|
// the read marker, so this room is definitely unread.
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -17,58 +17,11 @@ limitations under the License.
|
||||||
|
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
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.
|
* 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 {
|
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() {
|
loadProfileInfo: function() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
return cli.getProfileInfo(cli.credentials.userId);
|
return cli.getProfileInfo(cli.credentials.userId);
|
||||||
|
@ -91,36 +44,6 @@ export default {
|
||||||
// TODO
|
// 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) {
|
changePassword: function(oldPassword, newPassword) {
|
||||||
const cli = MatrixClientPeg.get();
|
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
|
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
326
src/WidgetMessaging.js
Normal 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,
|
||||||
|
};
|
|
@ -26,7 +26,7 @@ import {PillCompletion} from './Components';
|
||||||
import type {SelectionRange, Completion} from './Autocompleter';
|
import type {SelectionRange, Completion} from './Autocompleter';
|
||||||
import _uniq from 'lodash/uniq';
|
import _uniq from 'lodash/uniq';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import UserSettingsStore from '../UserSettingsStore';
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
|
||||||
import EmojiData from '../stripped-emoji.json';
|
import EmojiData from '../stripped-emoji.json';
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange) {
|
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
|
return []; // don't give any suggestions if the user doesn't want them
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,9 +53,11 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
|
MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
|
MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
|
@ -33,6 +33,7 @@ module.exports = {
|
||||||
menuHeight: React.PropTypes.number,
|
menuHeight: React.PropTypes.number,
|
||||||
chevronOffset: React.PropTypes.number,
|
chevronOffset: React.PropTypes.number,
|
||||||
menuColour: React.PropTypes.string,
|
menuColour: React.PropTypes.string,
|
||||||
|
chevronFace: React.PropTypes.string, // top, bottom, left, right
|
||||||
},
|
},
|
||||||
|
|
||||||
getOrCreateContainer: function() {
|
getOrCreateContainer: function() {
|
||||||
|
@ -58,12 +59,30 @@ module.exports = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const position = {
|
const position = {};
|
||||||
top: props.top,
|
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 = {};
|
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;
|
chevronOffset.top = props.chevronOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,28 +93,27 @@ module.exports = {
|
||||||
.mx_ContextualMenu_chevron_left:after {
|
.mx_ContextualMenu_chevron_left:after {
|
||||||
border-right-color: ${props.menuColour};
|
border-right-color: ${props.menuColour};
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_chevron_right:after {
|
.mx_ContextualMenu_chevron_right:after {
|
||||||
border-left-color: ${props.menuColour};
|
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;
|
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace}></div>;
|
||||||
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 className = 'mx_ContextualMenu_wrapper';
|
const className = 'mx_ContextualMenu_wrapper';
|
||||||
|
|
||||||
const menuClasses = classNames({
|
const menuClasses = classNames({
|
||||||
'mx_ContextualMenu': true,
|
'mx_ContextualMenu': true,
|
||||||
'mx_ContextualMenu_left': props.left,
|
'mx_ContextualMenu_left': chevronFace === 'left',
|
||||||
'mx_ContextualMenu_right': !props.left,
|
'mx_ContextualMenu_right': chevronFace === 'right',
|
||||||
|
'mx_ContextualMenu_top': chevronFace === 'top',
|
||||||
|
'mx_ContextualMenu_bottom': chevronFace === 'bottom',
|
||||||
});
|
});
|
||||||
|
|
||||||
const menuStyle = {};
|
const menuStyle = {};
|
||||||
|
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import { _t, _tJsx } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Component which shows the filtered file using a TimelinePanel
|
* Component which shows the filtered file using a TimelinePanel
|
||||||
|
@ -92,7 +92,10 @@ const FilePanel = React.createClass({
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||||
<div className="mx_RoomView_empty">
|
<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>
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.noRoom) {
|
} else if (this.noRoom) {
|
||||||
|
|
|
@ -22,7 +22,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import { sanitizedHtmlNode } from '../../HtmlUtils';
|
import { sanitizedHtmlNode } from '../../HtmlUtils';
|
||||||
import { _t, _td, _tJsx } from '../../languageHandler';
|
import { _t, _td } from '../../languageHandler';
|
||||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
@ -430,18 +430,20 @@ export default React.createClass({
|
||||||
uploadingAvatar: false,
|
uploadingAvatar: false,
|
||||||
membershipBusy: false,
|
membershipBusy: false,
|
||||||
publicityBusy: false,
|
publicityBusy: false,
|
||||||
|
inviterProfile: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._matrixClient = MatrixClientPeg.get();
|
||||||
|
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
|
||||||
this._changeAvatarComponent = null;
|
this._changeAvatarComponent = null;
|
||||||
this._initGroupStore(this.props.groupId, true);
|
this._initGroupStore(this.props.groupId, true);
|
||||||
|
|
||||||
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
this._groupStore.removeAllListeners();
|
this._groupStore.removeAllListeners();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -463,7 +465,11 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_initGroupStore: function(groupId, firstInit) {
|
_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(() => {
|
this._groupStore.registerListener(() => {
|
||||||
const summary = this._groupStore.getSummary();
|
const summary = this._groupStore.getSummary();
|
||||||
if (summary.profile) {
|
if (summary.profile) {
|
||||||
|
@ -481,7 +487,7 @@ export default React.createClass({
|
||||||
groupRooms: this._groupStore.getGroupRooms(),
|
groupRooms: this._groupStore.getGroupRooms(),
|
||||||
groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
|
groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
|
||||||
isUserMember: this._groupStore.getGroupMembers().some(
|
isUserMember: this._groupStore.getGroupMembers().some(
|
||||||
(m) => m.userId === MatrixClientPeg.get().credentials.userId,
|
(m) => m.userId === this._matrixClient.credentials.userId,
|
||||||
),
|
),
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
@ -489,7 +495,19 @@ export default React.createClass({
|
||||||
this._onEditClick();
|
this._onEditClick();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let willDoOnboarding = false;
|
||||||
this._groupStore.on('error', (err) => {
|
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({
|
this.setState({
|
||||||
summary: null,
|
summary: null,
|
||||||
error: err,
|
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) {
|
_onShowRhsClick: function(ev) {
|
||||||
dis.dispatch({ action: 'show_right_panel' });
|
dis.dispatch({ action: 'show_right_panel' });
|
||||||
},
|
},
|
||||||
|
@ -546,7 +584,7 @@ export default React.createClass({
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
this.setState({uploadingAvatar: true});
|
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 });
|
const newProfileForm = Object.assign(this.state.profileForm, { avatar_url: url });
|
||||||
this.setState({
|
this.setState({
|
||||||
uploadingAvatar: false,
|
uploadingAvatar: false,
|
||||||
|
@ -566,7 +604,7 @@ export default React.createClass({
|
||||||
_onSaveClick: function() {
|
_onSaveClick: function() {
|
||||||
this.setState({saving: true});
|
this.setState({saving: true});
|
||||||
const savePromise = this.state.isUserPrivileged ?
|
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();
|
Promise.resolve();
|
||||||
savePromise.then((result) => {
|
savePromise.then((result) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -591,7 +629,7 @@ export default React.createClass({
|
||||||
|
|
||||||
_onAcceptInviteClick: function() {
|
_onAcceptInviteClick: function() {
|
||||||
this.setState({membershipBusy: true});
|
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
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
|
@ -605,7 +643,7 @@ export default React.createClass({
|
||||||
|
|
||||||
_onRejectInviteClick: function() {
|
_onRejectInviteClick: function() {
|
||||||
this.setState({membershipBusy: true});
|
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
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
|
@ -628,7 +666,7 @@ export default React.createClass({
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
this.setState({membershipBusy: true});
|
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
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
|
@ -646,18 +684,6 @@ export default React.createClass({
|
||||||
showGroupAddRoomDialog(this.props.groupId);
|
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() {
|
_getGroupSection: function() {
|
||||||
const groupSettingsSectionClasses = classnames({
|
const groupSettingsSectionClasses = classnames({
|
||||||
"mx_GroupView_group": this.state.editing,
|
"mx_GroupView_group": this.state.editing,
|
||||||
|
@ -802,20 +828,37 @@ export default React.createClass({
|
||||||
|
|
||||||
_getMembershipSection: function() {
|
_getMembershipSection: function() {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
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) return null;
|
||||||
|
|
||||||
if (group.myMembership === 'invite') {
|
if (group.myMembership === 'invite') {
|
||||||
if (this.state.membershipBusy) {
|
if (this.state.membershipBusy || this.state.inviterProfileBusy) {
|
||||||
return <div className="mx_GroupView_membershipSection">
|
return <div className="mx_GroupView_membershipSection">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>;
|
</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">
|
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited">
|
||||||
<div className="mx_GroupView_membershipSubSection">
|
<div className="mx_GroupView_membershipSubSection">
|
||||||
<div className="mx_GroupView_membershipSection_description">
|
<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>
|
||||||
<div className="mx_GroupView_membership_buttonContainer">
|
<div className="mx_GroupView_membership_buttonContainer">
|
||||||
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
|
@ -860,25 +903,6 @@ export default React.createClass({
|
||||||
return null;
|
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() {
|
_getLongDescriptionNode: function() {
|
||||||
const summary = this.state.summary;
|
const summary = this.state.summary;
|
||||||
let description = null;
|
let description = null;
|
||||||
|
@ -889,12 +913,12 @@ export default React.createClass({
|
||||||
className="mx_GroupView_groupDesc_placeholder"
|
className="mx_GroupView_groupDesc_placeholder"
|
||||||
onClick={this._onEditClick}
|
onClick={this._onEditClick}
|
||||||
>
|
>
|
||||||
{ _tJsx(
|
{ _t(
|
||||||
'Your community hasn\'t got a Long Description, a HTML page to show to community members.<br />' +
|
'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!',
|
'Click here to open settings and give it one!',
|
||||||
[/<br \/>/],
|
{},
|
||||||
[(sub) => <br />])
|
{ 'br': <br /> },
|
||||||
}
|
) }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
const groupDescEditingClasses = classnames({
|
const groupDescEditingClasses = classnames({
|
||||||
|
@ -933,7 +957,6 @@ export default React.createClass({
|
||||||
let shortDescNode;
|
let shortDescNode;
|
||||||
const bodyNodes = [
|
const bodyNodes = [
|
||||||
this._getMembershipSection(),
|
this._getMembershipSection(),
|
||||||
this.state.editing ? this._getMemberSettingsSection() : null,
|
|
||||||
this._getGroupSection(),
|
this._getGroupSection(),
|
||||||
];
|
];
|
||||||
const rightButtons = [];
|
const rightButtons = [];
|
||||||
|
|
|
@ -19,8 +19,7 @@ limitations under the License.
|
||||||
import * as Matrix from 'matrix-js-sdk';
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import UserSettingsStore from '../../UserSettingsStore';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
import KeyCode from '../../KeyCode';
|
|
||||||
import Notifier from '../../Notifier';
|
import Notifier from '../../Notifier';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
import CallMediaHandler from '../../CallMediaHandler';
|
import CallMediaHandler from '../../CallMediaHandler';
|
||||||
|
@ -28,6 +27,7 @@ import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import sessionStore from '../../stores/SessionStore';
|
import sessionStore from '../../stores/SessionStore';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
* 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() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
// use compact timeline view
|
// use compact timeline view
|
||||||
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
|
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -153,13 +153,7 @@ export default React.createClass({
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let handled = false;
|
let handled = false;
|
||||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||||
let ctrlCmdOnly;
|
|
||||||
if (isMac) {
|
|
||||||
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
|
||||||
} else {
|
|
||||||
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.UP:
|
case KeyCode.UP:
|
||||||
|
@ -213,6 +207,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const TagPanel = sdk.getComponent('structures.TagPanel');
|
||||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||||
const RoomView = sdk.getComponent('structures.RoomView');
|
const RoomView = sdk.getComponent('structures.RoomView');
|
||||||
|
@ -334,6 +329,7 @@ export default React.createClass({
|
||||||
<div className='mx_MatrixChat_wrapper'>
|
<div className='mx_MatrixChat_wrapper'>
|
||||||
{ topBar }
|
{ topBar }
|
||||||
<div className={bodyClasses}>
|
<div className={bodyClasses}>
|
||||||
|
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
selectedRoom={this.props.currentRoomId}
|
selectedRoom={this.props.currentRoomId}
|
||||||
collapsed={this.props.collapseLhs || false}
|
collapsed={this.props.collapseLhs || false}
|
||||||
|
|
|
@ -22,7 +22,6 @@ import React from 'react';
|
||||||
import Matrix from "matrix-js-sdk";
|
import Matrix from "matrix-js-sdk";
|
||||||
|
|
||||||
import Analytics from "../../Analytics";
|
import Analytics from "../../Analytics";
|
||||||
import UserSettingsStore from '../../UserSettingsStore';
|
|
||||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||||
import PlatformPeg from "../../PlatformPeg";
|
import PlatformPeg from "../../PlatformPeg";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
|
@ -44,6 +43,7 @@ import createRoom from "../../createRoom";
|
||||||
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
|
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
|
||||||
import KeyRequestHandler from '../../KeyRequestHandler';
|
import KeyRequestHandler from '../../KeyRequestHandler';
|
||||||
import { _t, getCurrentLanguage } from '../../languageHandler';
|
import { _t, getCurrentLanguage } from '../../languageHandler';
|
||||||
|
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||||
|
|
||||||
/** constants for MatrixChat.state.view */
|
/** constants for MatrixChat.state.view */
|
||||||
const VIEWS = {
|
const VIEWS = {
|
||||||
|
@ -74,6 +74,16 @@ const VIEWS = {
|
||||||
LOGGED_IN: 6,
|
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({
|
module.exports = React.createClass({
|
||||||
// we export this so that the integration tests can use it :-S
|
// we export this so that the integration tests can use it :-S
|
||||||
statics: {
|
statics: {
|
||||||
|
@ -213,7 +223,7 @@ module.exports = React.createClass({
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
SdkConfig.put(this.props.config);
|
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
|
// Used by _viewRoom before getting state from sync
|
||||||
this.firstSyncComplete = false;
|
this.firstSyncComplete = false;
|
||||||
|
@ -276,6 +286,11 @@ module.exports = React.createClass({
|
||||||
this._windowWidth = 10000;
|
this._windowWidth = 10000;
|
||||||
this.handleResize();
|
this.handleResize();
|
||||||
window.addEventListener('resize', 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() {
|
componentDidMount: function() {
|
||||||
|
@ -374,6 +389,22 @@ module.exports = React.createClass({
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
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) {
|
switch (payload.action) {
|
||||||
case 'logout':
|
case 'logout':
|
||||||
Lifecycle.logout();
|
Lifecycle.logout();
|
||||||
|
@ -463,22 +494,17 @@ module.exports = React.createClass({
|
||||||
this._viewIndexedRoom(payload.roomIndex);
|
this._viewIndexedRoom(payload.roomIndex);
|
||||||
break;
|
break;
|
||||||
case 'view_user_settings':
|
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._setPage(PageTypes.UserSettings);
|
||||||
this.notifyNewScreen('settings');
|
this.notifyNewScreen('settings');
|
||||||
break;
|
break;
|
||||||
case 'view_create_room':
|
case 'view_create_room':
|
||||||
this._createRoom();
|
this._createRoom();
|
||||||
break;
|
break;
|
||||||
|
case 'view_create_group': {
|
||||||
|
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
||||||
|
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'view_room_directory':
|
case 'view_room_directory':
|
||||||
this._setPage(PageTypes.RoomDirectory);
|
this._setPage(PageTypes.RoomDirectory);
|
||||||
this.notifyNewScreen('directory');
|
this.notifyNewScreen('directory');
|
||||||
|
@ -509,7 +535,7 @@ module.exports = React.createClass({
|
||||||
this._chatCreateOrReuse(payload.user_id, payload.go_home_on_cancel);
|
this._chatCreateOrReuse(payload.user_id, payload.go_home_on_cancel);
|
||||||
break;
|
break;
|
||||||
case 'view_create_chat':
|
case 'view_create_chat':
|
||||||
this._createChat();
|
showStartChatInviteDialog();
|
||||||
break;
|
break;
|
||||||
case 'view_invite':
|
case 'view_invite':
|
||||||
showRoomInviteDialog(payload.roomId);
|
showRoomInviteDialog(payload.roomId);
|
||||||
|
@ -569,6 +595,9 @@ module.exports = React.createClass({
|
||||||
this._onWillStartClient();
|
this._onWillStartClient();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'client_started':
|
||||||
|
this._onClientStarted();
|
||||||
|
break;
|
||||||
case 'new_version':
|
case 'new_version':
|
||||||
this.onVersion(
|
this.onVersion(
|
||||||
payload.currentVersion, payload.newVersion,
|
payload.currentVersion, payload.newVersion,
|
||||||
|
@ -750,31 +779,7 @@ module.exports = React.createClass({
|
||||||
}).close;
|
}).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() {
|
_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');
|
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
||||||
Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
|
Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
|
||||||
onFinished: (shouldCreate, name, noFederate) => {
|
onFinished: (shouldCreate, name, noFederate) => {
|
||||||
|
@ -890,7 +895,7 @@ module.exports = React.createClass({
|
||||||
*/
|
*/
|
||||||
_onSetTheme: function(theme) {
|
_onSetTheme: function(theme) {
|
||||||
if (!theme) {
|
if (!theme) {
|
||||||
theme = 'light';
|
theme = SettingsStore.getValue("theme");
|
||||||
}
|
}
|
||||||
|
|
||||||
// look for the stylesheet elements.
|
// 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
|
// 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
|
// bothers to do an update on a true->false transition, so this ensures
|
||||||
// that we get exactly one update, at the right time.
|
// 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;
|
styleElements[theme].disabled = false;
|
||||||
|
|
||||||
if (theme === 'dark') {
|
const switchTheme = function() {
|
||||||
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
// we re-enable our theme here just in case we raced with another
|
||||||
// XXX: obviously this shouldn't be hardcoded here.
|
// theme set request as per https://github.com/vector-im/riot-web/issues/5601.
|
||||||
Tinter.tintSvgWhite('#2d2d2d');
|
// We could alternatively lock or similar to stop the race, but
|
||||||
} else {
|
// this is probably good enough for now.
|
||||||
Tinter.tintSvgWhite('#ffffff');
|
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) => {
|
cli.on("crypto.roomKeyRequestCancellation", (req) => {
|
||||||
krh.handleKeyRequestCancellation(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) {
|
showScreen: function(screen, params) {
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import UserSettingsStore from '../../UserSettingsStore';
|
|
||||||
import shouldHideEvent from '../../shouldHideEvent';
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
import dis from "../../dispatcher";
|
import dis from "../../dispatcher";
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
|
@ -110,8 +109,6 @@ module.exports = React.createClass({
|
||||||
// Velocity requires
|
// Velocity requires
|
||||||
this._readMarkerGhostNode = null;
|
this._readMarkerGhostNode = null;
|
||||||
|
|
||||||
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
|
||||||
|
|
||||||
this._isMounted = true;
|
this._isMounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -251,7 +248,7 @@ module.exports = React.createClass({
|
||||||
// Always show highlighted event
|
// Always show highlighted event
|
||||||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||||
|
|
||||||
return !shouldHideEvent(mxEv, this._syncedSettings);
|
return !shouldHideEvent(mxEv);
|
||||||
},
|
},
|
||||||
|
|
||||||
_getEventTiles: function() {
|
_getEventTiles: function() {
|
||||||
|
|
|
@ -15,69 +15,12 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
import {MatrixClient} from 'matrix-js-sdk';
|
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import { _t, _tJsx } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
import dis from '../../dispatcher';
|
||||||
import withMatrixClient from '../../wrappers/withMatrixClient';
|
import withMatrixClient from '../../wrappers/withMatrixClient';
|
||||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
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({
|
export default withMatrixClient(React.createClass({
|
||||||
displayName: 'MyGroups',
|
displayName: 'MyGroups',
|
||||||
|
@ -98,14 +41,18 @@ export default withMatrixClient(React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onCreateGroupClick: function() {
|
_onCreateGroupClick: function() {
|
||||||
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
dis.dispatch({action: 'view_create_group'});
|
||||||
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_fetch: function() {
|
_fetch: function() {
|
||||||
this.props.matrixClient.getJoinedGroups().done((result) => {
|
this.props.matrixClient.getJoinedGroups().done((result) => {
|
||||||
this.setState({groups: result.groups, error: null});
|
this.setState({groups: result.groups, error: null});
|
||||||
}, (err) => {
|
}, (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});
|
this.setState({groups: null, error: err});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -114,6 +61,7 @@ export default withMatrixClient(React.createClass({
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
const GroupTile = sdk.getComponent("groups.GroupTile");
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
let contentHeader;
|
let contentHeader;
|
||||||
|
@ -165,13 +113,13 @@ export default withMatrixClient(React.createClass({
|
||||||
<div className="mx_MyGroups_headerCard_header">
|
<div className="mx_MyGroups_headerCard_header">
|
||||||
{ _t('Join an existing community') }
|
{ _t('Join an existing community') }
|
||||||
</div>
|
</div>
|
||||||
{ _tJsx(
|
{ _t(
|
||||||
'To join an existing community you\'ll have to '+
|
'To join an existing community you\'ll have to '+
|
||||||
'know its community identifier; this will look '+
|
'know its community identifier; this will look '+
|
||||||
'something like <i>+example:matrix.org</i>.',
|
'something like <i>+example:matrix.org</i>.',
|
||||||
/<i>(.*)<\/i>/,
|
{},
|
||||||
(sub) => <i>{ sub }</i>,
|
{ 'i': (sub) => <i>{ sub }</i> })
|
||||||
) }
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,13 +15,12 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t, _tJsx } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import WhoIsTyping from '../../WhoIsTyping';
|
import WhoIsTyping from '../../WhoIsTyping';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||||
|
|
||||||
const HIDE_DEBOUNCE_MS = 10000;
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||||
|
@ -272,12 +271,16 @@ module.exports = React.createClass({
|
||||||
{ this.props.unsentMessageError }
|
{ this.props.unsentMessageError }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
<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>/],
|
_t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
|
||||||
[
|
"You can also select individual messages to resend or cancel.",
|
||||||
(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>,
|
{
|
||||||
],
|
'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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -322,12 +325,15 @@ module.exports = React.createClass({
|
||||||
if (this.props.sentMessageAndIsAlone) {
|
if (this.props.sentMessageAndIsAlone) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_isAlone">
|
<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>?",
|
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
|
||||||
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
"or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||||
[
|
{},
|
||||||
(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>,
|
'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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -29,7 +29,6 @@ const classNames = require("classnames");
|
||||||
const Matrix = require("matrix-js-sdk");
|
const Matrix = require("matrix-js-sdk");
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
const UserSettingsStore = require('../../UserSettingsStore');
|
|
||||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
const ContentMessages = require("../../ContentMessages");
|
const ContentMessages = require("../../ContentMessages");
|
||||||
const Modal = require("../../Modal");
|
const Modal = require("../../Modal");
|
||||||
|
@ -42,10 +41,11 @@ const rate_limited_func = require('../../ratelimitedfunc');
|
||||||
const ObjectUtils = require('../../ObjectUtils');
|
const ObjectUtils = require('../../ObjectUtils');
|
||||||
const Rooms = require('../../Rooms');
|
const Rooms = require('../../Rooms');
|
||||||
|
|
||||||
import KeyCode from '../../KeyCode';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
|
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function() {};
|
let debuglog = function() {};
|
||||||
|
@ -149,8 +149,6 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
|
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
|
||||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||||
|
|
||||||
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
|
||||||
|
|
||||||
// Start listening for RoomViewStore updates
|
// Start listening for RoomViewStore updates
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||||
this._onRoomViewStoreUpdate(true);
|
this._onRoomViewStoreUpdate(true);
|
||||||
|
@ -305,7 +303,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// Check if user has previously chosen to hide the app drawer for this
|
// Check if user has previously chosen to hide the app drawer for this
|
||||||
// room. If so, do not show apps
|
// room. If so, do not show apps
|
||||||
let hideWidgetDrawer = localStorage.getItem(
|
const hideWidgetDrawer = localStorage.getItem(
|
||||||
room.roomId + "_hide_widget_drawer");
|
room.roomId + "_hide_widget_drawer");
|
||||||
|
|
||||||
if (hideWidgetDrawer === "true") {
|
if (hideWidgetDrawer === "true") {
|
||||||
|
@ -435,13 +433,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onKeyDown: function(ev) {
|
onKeyDown: function(ev) {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||||
let ctrlCmdOnly;
|
|
||||||
if (isMac) {
|
|
||||||
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
|
||||||
} else {
|
|
||||||
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.KEY_D:
|
case KeyCode.KEY_D:
|
||||||
|
@ -542,7 +534,7 @@ module.exports = React.createClass({
|
||||||
// update unread count when scrolled up
|
// update unread count when scrolled up
|
||||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||||
// no change
|
// no change
|
||||||
} else if (!shouldHideEvent(ev, this._syncedSettings)) {
|
} else if (!shouldHideEvent(ev)) {
|
||||||
this.setState((state, props) => {
|
this.setState((state, props) => {
|
||||||
return {numUnreadMessages: state.numUnreadMessages + 1};
|
return {numUnreadMessages: state.numUnreadMessages + 1};
|
||||||
});
|
});
|
||||||
|
@ -616,38 +608,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_updatePreviewUrlVisibility: function(room) {
|
_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({
|
this.setState({
|
||||||
showUrlPreview: !roomPreviewUrls.getContent().disable,
|
showUrlPreview: SettingsStore.getValue("urlPreviewsEnabled", room.roomId),
|
||||||
});
|
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -666,12 +628,7 @@ module.exports = React.createClass({
|
||||||
const room = this.state.room;
|
const room = this.state.room;
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
const color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
|
const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
|
||||||
let color_scheme = {};
|
|
||||||
if (color_scheme_event) {
|
|
||||||
color_scheme = color_scheme_event.getContent();
|
|
||||||
// XXX: we should validate the event
|
|
||||||
}
|
|
||||||
console.log("Tinter.tint from updateTint");
|
console.log("Tinter.tint from updateTint");
|
||||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||||
},
|
},
|
||||||
|
@ -750,7 +707,7 @@ module.exports = React.createClass({
|
||||||
return;
|
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});
|
this.setState({isAlone: joinedMembers.length === 1});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1775,7 +1732,7 @@ module.exports = React.createClass({
|
||||||
const messagePanel = (
|
const messagePanel = (
|
||||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||||
showReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
showReadReceipts={!SettingsStore.getValue('hideReadReceipts')}
|
||||||
manageReadReceipts={!this.state.isPeeking}
|
manageReadReceipts={!this.state.isPeeking}
|
||||||
manageReadMarkers={!this.state.isPeeking}
|
manageReadMarkers={!this.state.isPeeking}
|
||||||
hidden={hideMessagePanel}
|
hidden={hideMessagePanel}
|
||||||
|
|
|
@ -18,7 +18,7 @@ const React = require("react");
|
||||||
const ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
const KeyCode = require('../../KeyCode');
|
import { KeyCode } from '../../Keyboard';
|
||||||
|
|
||||||
const DEBUG_SCROLL = false;
|
const DEBUG_SCROLL = false;
|
||||||
// var DEBUG_SCROLL = true;
|
// var DEBUG_SCROLL = true;
|
||||||
|
|
176
src/components/structures/TagPanel.js
Normal file
176
src/components/structures/TagPanel.js
Normal 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>;
|
||||||
|
},
|
||||||
|
});
|
|
@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
@ -29,8 +31,7 @@ const dis = require("../../dispatcher");
|
||||||
const ObjectUtils = require('../../ObjectUtils');
|
const ObjectUtils = require('../../ObjectUtils');
|
||||||
const Modal = require("../../Modal");
|
const Modal = require("../../Modal");
|
||||||
const UserActivity = require("../../UserActivity");
|
const UserActivity = require("../../UserActivity");
|
||||||
const KeyCode = require('../../KeyCode');
|
import { KeyCode } from '../../Keyboard';
|
||||||
import UserSettingsStore from '../../UserSettingsStore';
|
|
||||||
|
|
||||||
const PAGINATE_SIZE = 20;
|
const PAGINATE_SIZE = 20;
|
||||||
const INITIAL_SIZE = 20;
|
const INITIAL_SIZE = 20;
|
||||||
|
@ -129,8 +130,6 @@ var TimelinePanel = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events: [],
|
events: [],
|
||||||
timelineLoading: true, // track whether our room timeline is loading
|
timelineLoading: true, // track whether our room timeline is loading
|
||||||
|
@ -175,10 +174,10 @@ var TimelinePanel = React.createClass({
|
||||||
clientSyncState: MatrixClientPeg.get().getSyncState(),
|
clientSyncState: MatrixClientPeg.get().getSyncState(),
|
||||||
|
|
||||||
// should the event tiles have twelve hour times
|
// should the event tiles have twelve hour times
|
||||||
isTwelveHour: syncedSettings.showTwelveHourTimestamps,
|
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||||
|
|
||||||
// always show timestamps on event tiles?
|
// always show timestamps on event tiles?
|
||||||
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
|
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
const sdk = require('../../index');
|
const sdk = require('../../index');
|
||||||
|
@ -56,133 +58,69 @@ const gHVersionLabel = function(repo, token='') {
|
||||||
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
|
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enumerate some simple 'flip a bit' UI settings (if any).
|
// Enumerate some simple 'flip a bit' UI settings (if any). The strings provided here
|
||||||
// 'id' gives the key name in the im.vector.web.settings account data event
|
// must be settings defined in SettingsStore.
|
||||||
// 'label' is how we describe it in the UI.
|
const SIMPLE_SETTINGS = [
|
||||||
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
|
{ id: "urlPreviewsEnabled" },
|
||||||
// since they will be translated when rendered.
|
{ id: "autoplayGifsAndVideos" },
|
||||||
const SETTINGS_LABELS = [
|
{ id: "hideReadReceipts" },
|
||||||
{
|
{ id: "dontSendTypingNotifications" },
|
||||||
id: 'autoplayGifsAndVideos',
|
{ id: "alwaysShowTimestamps" },
|
||||||
label: _td('Autoplay GIFs and videos'),
|
{ id: "showTwelveHourTimestamps" },
|
||||||
},
|
{ id: "hideJoinLeaves" },
|
||||||
{
|
{ id: "hideAvatarChanges" },
|
||||||
id: 'hideReadReceipts',
|
{ id: "hideDisplaynameChanges" },
|
||||||
label: _td('Hide read receipts'),
|
{ id: "useCompactLayout" },
|
||||||
},
|
{ id: "hideRedactions" },
|
||||||
{
|
{ id: "enableSyntaxHighlightLanguageDetection" },
|
||||||
id: 'dontSendTypingNotifications',
|
{ id: "MessageComposerInput.autoReplaceEmoji" },
|
||||||
label: _td("Don't send typing notifications"),
|
{ id: "MessageComposerInput.dontSuggestEmoji" },
|
||||||
},
|
{ id: "Pill.shouldHidePillAvatar" },
|
||||||
{
|
{ id: "TextualBody.disableBigEmoji" },
|
||||||
id: 'alwaysShowTimestamps',
|
{ id: "VideoView.flipVideoHorizontally" },
|
||||||
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',
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const ANALYTICS_SETTINGS_LABELS = [
|
// These settings must be defined in SettingsStore
|
||||||
|
const ANALYTICS_SETTINGS = [
|
||||||
{
|
{
|
||||||
id: 'analyticsOptOut',
|
id: 'analyticsOptOut',
|
||||||
label: _td('Opt out of analytics'),
|
|
||||||
fn: function(checked) {
|
fn: function(checked) {
|
||||||
Analytics[checked ? 'disable' : 'enable']();
|
Analytics[checked ? 'disable' : 'enable']();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const WEBRTC_SETTINGS_LABELS = [
|
// These settings must be defined in SettingsStore
|
||||||
|
const WEBRTC_SETTINGS = [
|
||||||
{
|
{
|
||||||
id: 'webRtcForceTURN',
|
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,
|
// These settings must be defined in SettingsStore
|
||||||
// since they will be translated when rendered.
|
const CRYPTO_SETTINGS = [
|
||||||
const CRYPTO_SETTINGS_LABELS = [
|
|
||||||
{
|
{
|
||||||
id: 'blacklistUnverifiedDevices',
|
id: 'blacklistUnverifiedDevices',
|
||||||
label: _td('Never send encrypted messages to unverified devices from this device'),
|
|
||||||
fn: function(checked) {
|
fn: function(checked) {
|
||||||
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(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.
|
// 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.
|
// '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
|
// 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.
|
// packaged up in a single directory, and/or located at the application layer.
|
||||||
// But for now for expedience we just hardcode them here.
|
// But for now for expedience we just hardcode them here.
|
||||||
const THEMES = [
|
const THEMES = [
|
||||||
{
|
{ label: _td('Light theme'), value: 'light' },
|
||||||
id: 'theme',
|
{ label: _td('Dark theme'), value: 'dark' },
|
||||||
label: _td('Light theme'),
|
{ label: _td('Status.im theme'), value: 'status' },
|
||||||
value: 'light',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'theme',
|
|
||||||
label: _td('Dark theme'),
|
|
||||||
value: 'dark',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const IgnoredUser = React.createClass({
|
const IgnoredUser = React.createClass({
|
||||||
|
@ -204,7 +142,7 @@ const IgnoredUser = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<AccessibleButton onClick={this._onUnignoreClick} className="mx_UserSettings_button mx_UserSettings_buttonSmall">
|
<AccessibleButton onClick={this._onUnignoreClick} className="mx_textButton">
|
||||||
{ _t("Unignore") }
|
{ _t("Unignore") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{ this.props.userId }
|
{ this.props.userId }
|
||||||
|
@ -281,14 +219,6 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
this._refreshFromServer();
|
this._refreshFromServer();
|
||||||
|
|
||||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
|
||||||
if (!syncedSettings.theme) {
|
|
||||||
syncedSettings.theme = 'light';
|
|
||||||
}
|
|
||||||
this._syncedSettings = syncedSettings;
|
|
||||||
|
|
||||||
this._localSettings = UserSettingsStore.getLocalSettings();
|
|
||||||
|
|
||||||
if (PlatformPeg.get().isElectron()) {
|
if (PlatformPeg.get().isElectron()) {
|
||||||
const {ipcRenderer} = require('electron');
|
const {ipcRenderer} = require('electron');
|
||||||
|
|
||||||
|
@ -359,8 +289,8 @@ module.exports = React.createClass({
|
||||||
if (this._unmounted) return;
|
if (this._unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
activeAudioInput: this._localSettings['webrtc_audioinput'],
|
activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
|
||||||
activeVideoInput: this._localSettings['webrtc_videoinput'],
|
activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -492,10 +422,6 @@ module.exports = React.createClass({
|
||||||
dis.dispatch({action: 'password_changed'});
|
dis.dispatch({action: 'password_changed'});
|
||||||
},
|
},
|
||||||
|
|
||||||
onEnableNotificationsChange: function(event) {
|
|
||||||
UserSettingsStore.setEnableNotifications(event.target.checked);
|
|
||||||
},
|
|
||||||
|
|
||||||
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
||||||
if (!shouldSubmit) return;
|
if (!shouldSubmit) return;
|
||||||
this._addEmail();
|
this._addEmail();
|
||||||
|
@ -669,6 +595,11 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderGroupSettings: function() {
|
||||||
|
const GroupUserSettings = sdk.getComponent('groups.GroupUserSettings');
|
||||||
|
return <GroupUserSettings />;
|
||||||
|
},
|
||||||
|
|
||||||
_renderReferral: function() {
|
_renderReferral: function() {
|
||||||
const teamToken = this.props.teamToken;
|
const teamToken = this.props.teamToken;
|
||||||
if (!teamToken) {
|
if (!teamToken) {
|
||||||
|
@ -692,7 +623,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onLanguageChange: function(newLang) {
|
onLanguageChange: function(newLang) {
|
||||||
if (this.state.language !== newLang) {
|
if (this.state.language !== newLang) {
|
||||||
UserSettingsStore.setLocalSetting('language', newLang);
|
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||||
this.setState({
|
this.setState({
|
||||||
language: newLang,
|
language: newLang,
|
||||||
});
|
});
|
||||||
|
@ -715,14 +646,13 @@ module.exports = React.createClass({
|
||||||
// TODO: this ought to be a separate component so that we don't need
|
// TODO: this ought to be a separate component so that we don't need
|
||||||
// to rebind the onChange each time we render
|
// to rebind the onChange each time we render
|
||||||
const onChange = (e) =>
|
const onChange = (e) =>
|
||||||
UserSettingsStore.setLocalSetting('autocompleteDelay', + e.target.value);
|
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>{ _t("User Interface") }</h3>
|
<h3>{ _t("User Interface") }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{ this._renderUrlPreviewSelector() }
|
{ SIMPLE_SETTINGS.map( this._renderAccountSetting ) }
|
||||||
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
|
{ THEMES.map( this._renderThemeOption ) }
|
||||||
{ THEMES.map( this._renderThemeSelector ) }
|
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -730,7 +660,7 @@ module.exports = React.createClass({
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
defaultValue={UserSettingsStore.getLocalSetting('autocompleteDelay', 200)}
|
defaultValue={SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay")}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
@ -743,69 +673,31 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderUrlPreviewSelector: function() {
|
_renderAccountSetting: function(setting) {
|
||||||
return <div className="mx_UserSettings_toggle">
|
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||||
<input id="urlPreviewsDisabled"
|
return (
|
||||||
type="checkbox"
|
<div className="mx_UserSettings_toggle" key={setting.id}>
|
||||||
defaultChecked={UserSettingsStore.getUrlPreviewsDisabled()}
|
<SettingsFlag name={setting.id}
|
||||||
onChange={this._onPreviewsDisabledChanged}
|
label={setting.label}
|
||||||
/>
|
level={SettingLevel.ACCOUNT}
|
||||||
<label htmlFor="urlPreviewsDisabled">
|
onChange={setting.fn} />
|
||||||
{ _t("Disable inline URL previews by default") }
|
</div>
|
||||||
</label>
|
);
|
||||||
</div>;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_onPreviewsDisabledChanged: function(e) {
|
_renderThemeOption: function(setting) {
|
||||||
UserSettingsStore.setUrlPreviewsDisabled(e.target.checked);
|
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||||
},
|
const onChange = (v) => dis.dispatch({action: 'set_theme', value: setting.value});
|
||||||
|
return (
|
||||||
_renderSyncedSetting: function(setting) {
|
<div className="mx_UserSettings_toggle" key={setting.id + '_' + setting.value}>
|
||||||
// TODO: this ought to be a separate component so that we don't need
|
<SettingsFlag name="theme"
|
||||||
// to rebind the onChange each time we render
|
label={setting.label}
|
||||||
|
level={SettingLevel.ACCOUNT}
|
||||||
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]}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
group="theme"
|
||||||
<label htmlFor={setting.id}>
|
value={setting.value} />
|
||||||
{ _t(setting.label) }
|
</div>
|
||||||
</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>;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderCryptoInfo: function() {
|
_renderCryptoInfo: function() {
|
||||||
|
@ -847,7 +739,7 @@ module.exports = React.createClass({
|
||||||
{ importExportButtons }
|
{ importExportButtons }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
|
{ CRYPTO_SETTINGS.map( this._renderDeviceSetting ) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -873,24 +765,16 @@ module.exports = React.createClass({
|
||||||
} else return (<div />);
|
} else return (<div />);
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderLocalSetting: function(setting) {
|
_renderDeviceSetting: function(setting) {
|
||||||
// TODO: this ought to be a separate component so that we don't need
|
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||||
// to rebind the onChange each time we render
|
return (
|
||||||
const onChange = (e) => {
|
<div className="mx_UserSettings_toggle" key={setting.id}>
|
||||||
UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
|
<SettingsFlag name={setting.id}
|
||||||
if (setting.fn) setting.fn(e.target.checked);
|
label={setting.label}
|
||||||
};
|
level={SettingLevel.DEVICE}
|
||||||
|
onChange={setting.fn} />
|
||||||
return <div className="mx_UserSettings_toggle" key={setting.id}>
|
</div>
|
||||||
<input id={setting.id}
|
);
|
||||||
type="checkbox"
|
|
||||||
defaultChecked={this._localSettings[setting.id]}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<label htmlFor={setting.id}>
|
|
||||||
{ _t(setting.label) }
|
|
||||||
</label>
|
|
||||||
</div>;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderDevicesPanel: function() {
|
_renderDevicesPanel: function() {
|
||||||
|
@ -927,18 +811,18 @@ module.exports = React.createClass({
|
||||||
<h3>{ _t('Analytics') }</h3>
|
<h3>{ _t('Analytics') }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
|
{ _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>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderLabs: function() {
|
_renderLabs: function() {
|
||||||
const features = [];
|
const features = [];
|
||||||
UserSettingsStore.getLabsFeatures().forEach((featureId) => {
|
SettingsStore.getLabsFeatures().forEach((featureId) => {
|
||||||
// TODO: this ought to be a separate component so that we don't need
|
// TODO: this ought to be a separate component so that we don't need
|
||||||
// to rebind the onChange each time we render
|
// to rebind the onChange each time we render
|
||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
UserSettingsStore.setFeatureEnabled(featureId, e.target.checked);
|
SettingsStore.setFeatureEnabled(featureId, e.target.checked);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -948,10 +832,10 @@ module.exports = React.createClass({
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={featureId}
|
id={featureId}
|
||||||
name={featureId}
|
name={featureId}
|
||||||
defaultChecked={UserSettingsStore.isFeatureEnabled(featureId)}
|
defaultChecked={SettingsStore.isFeatureEnabled(featureId)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={featureId}>{ UserSettingsStore.translatedNameForFeature(featureId) }</label>
|
<label htmlFor={featureId}>{ SettingsStore.getDisplayName(featureId) }</label>
|
||||||
</div>);
|
</div>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1044,6 +928,8 @@ module.exports = React.createClass({
|
||||||
const settings = this.state.electron_settings;
|
const settings = this.state.electron_settings;
|
||||||
if (!settings) return;
|
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>
|
return <div>
|
||||||
<h3>{ _t('Desktop specific') }</h3>
|
<h3>{ _t('Desktop specific') }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
|
@ -1166,7 +1052,7 @@ module.exports = React.createClass({
|
||||||
return <div>
|
return <div>
|
||||||
<h3>{ _t('VoIP') }</h3>
|
<h3>{ _t('VoIP') }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) }
|
{ WEBRTC_SETTINGS.map(this._renderDeviceSetting) }
|
||||||
{ this._renderWebRtcDeviceSettings() }
|
{ this._renderWebRtcDeviceSettings() }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -1368,6 +1254,8 @@ module.exports = React.createClass({
|
||||||
{ accountJsx }
|
{ accountJsx }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{ this._renderGroupSettings() }
|
||||||
|
|
||||||
{ this._renderReferral() }
|
{ this._renderReferral() }
|
||||||
|
|
||||||
{ notificationArea }
|
{ notificationArea }
|
||||||
|
|
|
@ -17,13 +17,13 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const React = require('react');
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
const sdk = require('../../../index');
|
import sdk from '../../../index';
|
||||||
const Modal = require("../../../Modal");
|
import Modal from "../../../Modal";
|
||||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
const PasswordReset = require("../../../PasswordReset");
|
import PasswordReset from "../../../PasswordReset";
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'ForgotPassword',
|
displayName: 'ForgotPassword',
|
||||||
|
@ -154,6 +154,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||||
|
@ -165,7 +166,7 @@ module.exports = React.createClass({
|
||||||
resetPasswordJsx = <Spinner />;
|
resetPasswordJsx = <Spinner />;
|
||||||
} else if (this.state.progress === "sent_email") {
|
} else if (this.state.progress === "sent_email") {
|
||||||
resetPasswordJsx = (
|
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 }) }
|
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||||
|
@ -174,7 +175,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
} else if (this.state.progress === "complete") {
|
} else if (this.state.progress === "complete") {
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
<div>
|
<div className="mx_Login_prompt">
|
||||||
<p>{ _t('Your password has been reset') }.</p>
|
<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>
|
<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}
|
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||||
|
@ -182,6 +183,20 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} 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 = (
|
resetPasswordJsx = (
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_Login_prompt">
|
<div className="mx_Login_prompt">
|
||||||
|
@ -209,16 +224,7 @@ module.exports = React.createClass({
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
|
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
|
||||||
</form>
|
</form>
|
||||||
<ServerConfig ref="serverConfig"
|
{ serverConfigSection }
|
||||||
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>
|
|
||||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||||
{ _t('Return to login screen') }
|
{ _t('Return to login screen') }
|
||||||
</a>
|
</a>
|
||||||
|
@ -233,12 +239,12 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_Login">
|
<LoginPage>
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
<LoginHeader />
|
<LoginHeader />
|
||||||
{ resetPasswordJsx }
|
{ resetPasswordJsx }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</LoginPage>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,12 +18,13 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as languageHandler from '../../../languageHandler';
|
import * as languageHandler from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Login from '../../../Login';
|
import Login from '../../../Login';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
|
||||||
import PlatformPeg from '../../../PlatformPeg';
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
// For validating phone numbers without country codes
|
// For validating phone numbers without country codes
|
||||||
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||||
|
@ -76,6 +77,14 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
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();
|
this._initLoginLogic();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -105,7 +114,22 @@ module.exports = React.createClass({
|
||||||
if (error.httpStatus == 400 && usingEmail) {
|
if (error.httpStatus == 400 && usingEmail) {
|
||||||
errorText = _t('This Home Server does not support login using email address.');
|
errorText = _t('This Home Server does not support login using email address.');
|
||||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
} 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.');
|
errorText = _t('Incorrect username and/or password.');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// other errors, not specific to doing a password login
|
// other errors, not specific to doing a password login
|
||||||
errorText = this._errorTextFromError(error);
|
errorText = this._errorTextFromError(error);
|
||||||
|
@ -217,13 +241,29 @@ module.exports = React.createClass({
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
loginLogic.getFlows().then(function(flows) {
|
loginLogic.getFlows().then((flows) => {
|
||||||
// old behaviour was to always use the first flow without presenting
|
// look for a flow where we understand all of the steps.
|
||||||
// options. This works in most cases (we don't have a UI for multiple
|
for (let i = 0; i < flows.length; i++ ) {
|
||||||
// logins so let's skip that for now).
|
if (!this._isSupportedFlow(flows[i])) {
|
||||||
loginLogic.chooseFlow(0);
|
continue;
|
||||||
self.setState({
|
}
|
||||||
currentFlow: self._getCurrentFlowStep(),
|
|
||||||
|
// 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) {
|
}, function(err) {
|
||||||
self.setState({
|
self.setState({
|
||||||
|
@ -237,6 +277,16 @@ module.exports = React.createClass({
|
||||||
}).done();
|
}).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() {
|
_getCurrentFlowStep: function() {
|
||||||
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
|
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
|
||||||
},
|
},
|
||||||
|
@ -256,17 +306,19 @@ module.exports = React.createClass({
|
||||||
!this.state.enteredHomeserverUrl.startsWith("http"))
|
!this.state.enteredHomeserverUrl.startsWith("http"))
|
||||||
) {
|
) {
|
||||||
errorText = <span>
|
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>.",
|
"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>;
|
</span>;
|
||||||
} else {
|
} else {
|
||||||
errorText = <span>
|
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>/,
|
_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.",
|
||||||
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; },
|
{},
|
||||||
|
{ 'a': (sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; } },
|
||||||
) }
|
) }
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
@ -276,8 +328,20 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentForStep: function(step) {
|
componentForStep: function(step) {
|
||||||
switch (step) {
|
if (!step) {
|
||||||
case 'm.login.password':
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepRenderer = this._stepRendererMap[step];
|
||||||
|
|
||||||
|
if (stepRenderer) {
|
||||||
|
return stepRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderPasswordStep: function() {
|
||||||
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
|
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
|
||||||
return (
|
return (
|
||||||
<PasswordLogin
|
<PasswordLogin
|
||||||
|
@ -293,26 +357,18 @@ module.exports = React.createClass({
|
||||||
hsUrl={this.state.enteredHomeserverUrl}
|
hsUrl={this.state.enteredHomeserverUrl}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'm.login.cas':
|
},
|
||||||
|
|
||||||
|
_renderCasStep: function() {
|
||||||
const CasLogin = sdk.getComponent('login.CasLogin');
|
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||||
return (
|
return (
|
||||||
<CasLogin onSubmit={this.onCasLogin} />
|
<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) {
|
_onLanguageChange: function(newLang) {
|
||||||
if (languageHandler.getCurrentLanguage() !== newLang) {
|
if (languageHandler.getCurrentLanguage() !== newLang) {
|
||||||
UserSettingsStore.setLocalSetting('language', newLang);
|
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||||
PlatformPeg.get().reload();
|
PlatformPeg.get().reload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -329,6 +385,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||||
|
@ -343,43 +400,68 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
let returnToAppJsx;
|
let returnToAppJsx;
|
||||||
|
/*
|
||||||
|
// with the advent of ILAG I don't think we need this any more
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
returnToAppJsx =
|
returnToAppJsx =
|
||||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||||
{ _t('Return to app') }
|
{ _t('Return to app') }
|
||||||
</a>;
|
</a>;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return (
|
let serverConfig;
|
||||||
<div className="mx_Login">
|
let header;
|
||||||
<div className="mx_Login_box">
|
|
||||||
<LoginHeader />
|
if (!SdkConfig.get().disable_custom_urls) {
|
||||||
<div>
|
serverConfig = <ServerConfig ref="serverConfig"
|
||||||
<h2>{ _t('Sign in') }
|
|
||||||
{ loader }
|
|
||||||
</h2>
|
|
||||||
{ this.componentForStep(this.state.currentFlow) }
|
|
||||||
<ServerConfig ref="serverConfig"
|
|
||||||
withToggleButton={true}
|
withToggleButton={true}
|
||||||
customHsUrl={this.props.customHsUrl}
|
customHsUrl={this.props.customHsUrl}
|
||||||
customIsUrl={this.props.customIsUrl}
|
customIsUrl={this.props.customIsUrl}
|
||||||
defaultHsUrl={this.props.defaultHsUrl}
|
defaultHsUrl={this.props.defaultHsUrl}
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
defaultIsUrl={this.props.defaultIsUrl}
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
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">
|
<div className="mx_Login_error">
|
||||||
{ this.state.errorText }
|
{ this.state.errorText }
|
||||||
</div>
|
</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="#">
|
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||||
{ _t('Create an account') }
|
{ _t('Create an account') }
|
||||||
</a>
|
</a>
|
||||||
{ loginAsGuestJsx }
|
{ loginAsGuestJsx }
|
||||||
{ returnToAppJsx }
|
{ returnToAppJsx }
|
||||||
{ this._renderLanguageSetting() }
|
{ !SdkConfig.get().disable_login_language_selector ? this._renderLanguageSetting() : '' }
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</LoginPage>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -59,9 +59,10 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||||
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||||
|
const LoginPage = sdk.getComponent('login.LoginPage');
|
||||||
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||||
return (
|
return (
|
||||||
<div className="mx_Login">
|
<LoginPage>
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
<LoginHeader />
|
<LoginHeader />
|
||||||
<div className="mx_Login_profile">
|
<div className="mx_Login_profile">
|
||||||
|
@ -74,7 +75,7 @@ module.exports = React.createClass({
|
||||||
{ this.state.errorString }
|
{ this.state.errorString }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</LoginPage>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,6 +26,8 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import RegistrationForm from '../../views/login/RegistrationForm';
|
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||||
import RtsClient from '../../../RtsClient';
|
import RtsClient from '../../../RtsClient';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
const MIN_PASSWORD_LENGTH = 6;
|
const MIN_PASSWORD_LENGTH = 6;
|
||||||
|
|
||||||
|
@ -302,7 +304,7 @@ module.exports = React.createClass({
|
||||||
} : {};
|
} : {};
|
||||||
|
|
||||||
return this._matrixClient.register(
|
return this._matrixClient.register(
|
||||||
this.state.formVals.username.toLowerCase(),
|
this.state.formVals.username,
|
||||||
this.state.formVals.password,
|
this.state.formVals.password,
|
||||||
undefined, // session id: included in the auth dict already
|
undefined, // session id: included in the auth dict already
|
||||||
auth,
|
auth,
|
||||||
|
@ -322,10 +324,13 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||||
const LoginFooter = sdk.getComponent('login.LoginFooter');
|
const LoginFooter = sdk.getComponent('login.LoginFooter');
|
||||||
|
const LoginPage = sdk.getComponent('login.LoginPage');
|
||||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
const ServerConfig = sdk.getComponent('views.login.ServerConfig');
|
const ServerConfig = sdk.getComponent('views.login.ServerConfig');
|
||||||
|
|
||||||
|
const theme = SettingsStore.getValue("theme");
|
||||||
|
|
||||||
let registerBody;
|
let registerBody;
|
||||||
if (this.state.doingUIAuth) {
|
if (this.state.doingUIAuth) {
|
||||||
registerBody = (
|
registerBody = (
|
||||||
|
@ -344,9 +349,19 @@ module.exports = React.createClass({
|
||||||
} else if (this.state.busy || this.state.teamServerBusy) {
|
} else if (this.state.busy || this.state.teamServerBusy) {
|
||||||
registerBody = <Spinner />;
|
registerBody = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
let errorSection;
|
let serverConfigSection;
|
||||||
if (this.state.errorText) {
|
if (!SdkConfig.get().disable_custom_urls) {
|
||||||
errorSection = <div className="mx_Login_error">{ this.state.errorText }</div>;
|
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 = (
|
registerBody = (
|
||||||
<div>
|
<div>
|
||||||
|
@ -362,21 +377,14 @@ module.exports = React.createClass({
|
||||||
onRegisterClick={this.onFormSubmit}
|
onRegisterClick={this.onFormSubmit}
|
||||||
onTeamSelected={this.onTeamSelected}
|
onTeamSelected={this.onTeamSelected}
|
||||||
/>
|
/>
|
||||||
{ errorSection }
|
{ 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}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let returnToAppJsx;
|
let returnToAppJsx;
|
||||||
|
/*
|
||||||
|
// with the advent of ILAG I don't think we need this any more
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
returnToAppJsx = (
|
returnToAppJsx = (
|
||||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||||
|
@ -384,8 +392,31 @@ module.exports = React.createClass({
|
||||||
</a>
|
</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 (
|
return (
|
||||||
<div className="mx_Login">
|
<LoginPage>
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
<LoginHeader
|
<LoginHeader
|
||||||
icon={this.state.teamSelected ?
|
icon={this.state.teamSelected ?
|
||||||
|
@ -393,15 +424,14 @@ module.exports = React.createClass({
|
||||||
this.state.teamSelected.domain + "/icon.png" :
|
this.state.teamSelected.domain + "/icon.png" :
|
||||||
null}
|
null}
|
||||||
/>
|
/>
|
||||||
<h2>{ _t('Create an account') }</h2>
|
{ header }
|
||||||
{ registerBody }
|
{ registerBody }
|
||||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
{ signIn }
|
||||||
{ _t('I already have an account') }
|
{ errorText }
|
||||||
</a>
|
|
||||||
{ returnToAppJsx }
|
{ returnToAppJsx }
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</LoginPage>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
168
src/components/views/avatars/MemberPresenceAvatar.js
Normal file
168
src/components/views/avatars/MemberPresenceAvatar.js
Normal 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;
|
||||||
|
},
|
||||||
|
});
|
|
@ -34,6 +34,8 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
description: PropTypes.node,
|
description: PropTypes.node,
|
||||||
|
// Extra node inserted after picker input, dropdown and errors
|
||||||
|
extraNode: PropTypes.node,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
roomId: PropTypes.string,
|
roomId: PropTypes.string,
|
||||||
|
@ -242,7 +244,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_doNaiveGroupRoomSearch: function(query) {
|
_doNaiveGroupRoomSearch: function(query) {
|
||||||
const lowerCaseQuery = query.toLowerCase();
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), this.props.groupId);
|
const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
||||||
const results = [];
|
const results = [];
|
||||||
groupStore.getGroupRooms().forEach((r) => {
|
groupStore.getGroupRooms().forEach((r) => {
|
||||||
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
@ -268,34 +270,53 @@ module.exports = React.createClass({
|
||||||
const rooms = MatrixClientPeg.get().getRooms();
|
const rooms = MatrixClientPeg.get().getRooms();
|
||||||
const results = [];
|
const results = [];
|
||||||
rooms.forEach((room) => {
|
rooms.forEach((room) => {
|
||||||
|
let rank = Infinity;
|
||||||
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
|
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
|
||||||
const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
|
|
||||||
const name = nameEvent ? nameEvent.getContent().name : '';
|
const name = nameEvent ? nameEvent.getContent().name : '';
|
||||||
const canonicalAlias = room.getCanonicalAlias();
|
const canonicalAlias = room.getCanonicalAlias();
|
||||||
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
|
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
|
||||||
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
|
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
|
||||||
return a.concat(b);
|
return a.concat(b);
|
||||||
}, []);
|
}, []);
|
||||||
const topic = topicEvent ? topicEvent.getContent().topic : '';
|
|
||||||
|
|
||||||
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
|
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
const aliasMatch = aliases.some((alias) =>
|
let aliasMatch = false;
|
||||||
(alias || '').toLowerCase().includes(lowerCaseQuery),
|
let shortestMatchingAliasLength = Infinity;
|
||||||
);
|
aliases.forEach((alias) => {
|
||||||
const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery);
|
if ((alias || '').toLowerCase().includes(lowerCaseQuery)) {
|
||||||
if (!(nameMatch || topicMatch || aliasMatch)) {
|
aliasMatch = true;
|
||||||
|
if (shortestMatchingAliasLength > alias.length) {
|
||||||
|
shortestMatchingAliasLength = alias.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(nameMatch || aliasMatch)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (aliasMatch) {
|
||||||
|
// A shorter matching alias will give a better rank
|
||||||
|
rank = shortestMatchingAliasLength;
|
||||||
|
}
|
||||||
|
|
||||||
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
|
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
|
||||||
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
|
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
|
rank,
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
avatar_url: avatarUrl,
|
avatar_url: avatarUrl,
|
||||||
name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
|
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({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
|
@ -574,6 +595,7 @@ module.exports = React.createClass({
|
||||||
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
|
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
|
||||||
{ error }
|
{ error }
|
||||||
{ addressSelector }
|
{ addressSelector }
|
||||||
|
{ this.props.extraNode }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import * as KeyCode from '../../../KeyCode';
|
import { KeyCode } from '../../../Keyboard';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import classnames from 'classnames';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -24,51 +23,17 @@ import { _t } from '../../../languageHandler';
|
||||||
*/
|
*/
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'ConfirmRedactDialog',
|
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() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||||
|
|
||||||
const title = _t("Confirm Removal");
|
|
||||||
|
|
||||||
const confirmButtonClass = classnames({
|
|
||||||
'mx_Dialog_primary': true,
|
|
||||||
'danger': false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
<QuestionDialog onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this.onOk}
|
title={_t("Confirm Removal")}
|
||||||
title={title}
|
description={
|
||||||
>
|
_t("Are you sure you wish to remove (delete) this event? " +
|
||||||
<div className="mx_Dialog_content">
|
|
||||||
{ _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.")}
|
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||||
</div>
|
button={_t("Remove")}>
|
||||||
<div className="mx_Dialog_buttons">
|
</QuestionDialog>
|
||||||
<button className={confirmButtonClass} onClick={this.onOk}>
|
|
||||||
{ _t("Remove") }
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onClick={this.onCancel}>
|
|
||||||
{ _t("Cancel") }
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</BaseDialog>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
@ -35,6 +36,8 @@ export default React.createClass({
|
||||||
member: React.PropTypes.object,
|
member: React.PropTypes.object,
|
||||||
// group member object. Supply either this or 'member'
|
// group member object. Supply either this or 'member'
|
||||||
groupMember: GroupMemberType,
|
groupMember: GroupMemberType,
|
||||||
|
// needed if a group member is specified
|
||||||
|
matrixClient: React.PropTypes.instanceOf(MatrixClient),
|
||||||
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
||||||
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
|
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||||
|
|
||||||
|
@ -104,10 +107,11 @@ export default React.createClass({
|
||||||
name = this.props.member.name;
|
name = this.props.member.name;
|
||||||
userId = this.props.member.userId;
|
userId = this.props.member.userId;
|
||||||
} else {
|
} else {
|
||||||
// we don't get this info from the API yet
|
const httpAvatarUrl = this.props.groupMember.avatarUrl ?
|
||||||
avatar = <BaseAvatar name={this.props.groupMember.userId} width={48} height={48} />;
|
this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null;
|
||||||
name = this.props.groupMember.userId;
|
name = this.props.groupMember.displayname || this.props.groupMember.userId;
|
||||||
userId = this.props.groupMember.userId;
|
userId = this.props.groupMember.userId;
|
||||||
|
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
|
@ -45,9 +45,10 @@ export default React.createClass({
|
||||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||||
bugreport = (
|
bugreport = (
|
||||||
<p>
|
<p>
|
||||||
{ _tJsx(
|
{ _t(
|
||||||
"Otherwise, <a>click here</a> to send a bug report.",
|
"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>
|
</p>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,8 +20,8 @@ import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import KeyCode from '../../../KeyCode';
|
import { KeyCode } from '../../../Keyboard';
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
// The amount of time to wait for further changes to the input username before
|
// The amount of time to wait for further changes to the input username before
|
||||||
// sending a request to the server
|
// sending a request to the server
|
||||||
|
@ -267,24 +267,21 @@ export default React.createClass({
|
||||||
</div>
|
</div>
|
||||||
{ usernameIndicator }
|
{ usernameIndicator }
|
||||||
<p>
|
<p>
|
||||||
{ _tJsx(
|
{ _t(
|
||||||
'This will be your account name on the <span></span> ' +
|
'This will be your account name on the <span></span> ' +
|
||||||
'homeserver, or you can pick a <a>different server</a>.',
|
'homeserver, or you can pick a <a>different server</a>.',
|
||||||
[
|
{},
|
||||||
/<span><\/span>/,
|
{
|
||||||
/<a>(.*?)<\/a>/,
|
'span': <span>{ this.props.homeserverUrl }</span>,
|
||||||
],
|
'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||||
[
|
},
|
||||||
(sub) => <span>{ this.props.homeserverUrl }</span>,
|
|
||||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
|
||||||
],
|
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{ _tJsx(
|
{ _t(
|
||||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
'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>
|
</p>
|
||||||
{ auth }
|
{ auth }
|
||||||
|
|
|
@ -20,6 +20,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
import Resend from '../../../Resend';
|
import Resend from '../../../Resend';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
function DeviceListEntry(props) {
|
function DeviceListEntry(props) {
|
||||||
const {userId, device} = props;
|
const {userId, device} = props;
|
||||||
|
@ -49,8 +50,7 @@ function UserUnknownDeviceList(props) {
|
||||||
|
|
||||||
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
|
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
|
||||||
<DeviceListEntry key={deviceId} userId={userId}
|
<DeviceListEntry key={deviceId} userId={userId}
|
||||||
device={userDevices[deviceId]}
|
device={userDevices[deviceId]} />,
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -93,66 +93,28 @@ export default React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
// map from userid -> deviceid -> deviceinfo
|
||||||
|
devices: React.PropTypes.object.isRequired,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentDidMount: 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given we've now shown the user the unknown device, it is no longer
|
// Given we've now shown the user the unknown device, it is no longer
|
||||||
// unknown to them. Therefore mark it as 'known'.
|
// 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);
|
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
// XXX: temporary logging to try to diagnose
|
||||||
devices: unknownDevices,
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
});
|
console.log('Opening UnknownDeviceDialog');
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
this._unmounted = true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
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;
|
let warning;
|
||||||
if (blacklistUnverified) {
|
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
|
||||||
warning = (
|
warning = (
|
||||||
<h4>
|
<h4>
|
||||||
{ _t("You are currently blacklisting unverified devices; to send " +
|
{ _t("You are currently blacklisting unverified devices; to send " +
|
||||||
|
@ -189,7 +151,7 @@ export default React.createClass({
|
||||||
{ warning }
|
{ warning }
|
||||||
{ _t("Unknown devices") }:
|
{ _t("Unknown devices") }:
|
||||||
|
|
||||||
<UnknownDeviceList devices={this.state.devices} />
|
<UnknownDeviceList devices={this.props.devices} />
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" autoFocus={true}
|
<button className="mx_Dialog_primary" autoFocus={true}
|
||||||
|
|
|
@ -77,6 +77,7 @@ export default React.createClass({
|
||||||
onClick={this._onClick}
|
onClick={this._onClick}
|
||||||
onMouseEnter={this._onMouseEnter}
|
onMouseEnter={this._onMouseEnter}
|
||||||
onMouseLeave={this._onMouseLeave}
|
onMouseLeave={this._onMouseLeave}
|
||||||
|
aria-label={this.props.label}
|
||||||
>
|
>
|
||||||
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
|
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
|
||||||
{ tooltip }
|
{ tooltip }
|
||||||
|
|
|
@ -17,10 +17,13 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
import qs from 'querystring';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import PlatformPeg from '../../../PlatformPeg';
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
|
import WidgetMessaging from '../../../WidgetMessaging';
|
||||||
|
import TintableSvgButton from './TintableSvgButton';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
|
@ -51,42 +54,92 @@ export default React.createClass({
|
||||||
creatorUserId: React.PropTypes.string,
|
creatorUserId: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps() {
|
||||||
return {
|
return {
|
||||||
url: "",
|
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);
|
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||||
return {
|
return {
|
||||||
loading: false,
|
initialising: true, // True while we are mangling the widget URL
|
||||||
widgetUrl: this.props.url,
|
loading: true, // True while the iframe content is loading
|
||||||
|
widgetUrl: this._addWurlParams(newProps.url),
|
||||||
widgetPermissionId: widgetPermissionId,
|
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
|
// Assume that widget has permission to load if we are the user who
|
||||||
hasPermissionToLoad: hasPermissionToLoad === 'true' || this.props.userId === this.props.creatorUserId,
|
// added it to the room, or if explicitly granted by the user
|
||||||
|
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
|
||||||
error: null,
|
error: null,
|
||||||
deleting: false,
|
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;
|
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
|
||||||
if (!scalarUrls || scalarUrls.length == 0) {
|
if (!scalarUrls || scalarUrls.length == 0) {
|
||||||
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < scalarUrls.length; i++) {
|
for (let i = 0; i < scalarUrls.length; i++) {
|
||||||
if (this.props.url.startsWith(scalarUrls[i])) {
|
if (url.startsWith(scalarUrls[i])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
isMixedContent: function() {
|
isMixedContent() {
|
||||||
const parentContentProtocol = window.location.protocol;
|
const parentContentProtocol = window.location.protocol;
|
||||||
const u = url.parse(this.props.url);
|
const u = url.parse(this.props.url);
|
||||||
const childContentProtocol = u.protocol;
|
const childContentProtocol = u.protocol;
|
||||||
|
@ -98,43 +151,77 @@ export default React.createClass({
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount() {
|
||||||
if (!this.isScalarUrl()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// Fetch the token before loading the iframe as we need to mangle the URL
|
|
||||||
this.setState({
|
// Fetch the token before loading the iframe as we need it to mangle the URL
|
||||||
loading: true,
|
if (!this._scalarClient) {
|
||||||
});
|
|
||||||
this._scalarClient = new ScalarAuthClient();
|
this._scalarClient = new ScalarAuthClient();
|
||||||
|
}
|
||||||
this._scalarClient.getScalarToken().done((token) => {
|
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;
|
this._scalarClient.scalarToken = token;
|
||||||
const u = url.parse(this.props.url);
|
const u = url.parse(this._addWurlParams(this.props.url));
|
||||||
if (!u.search) {
|
const params = qs.parse(u.query);
|
||||||
u.search = "?scalar_token=" + encodeURIComponent(token);
|
if (!params.scalar_token) {
|
||||||
} else {
|
params.scalar_token = encodeURIComponent(token);
|
||||||
u.search += "&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({
|
this.setState({
|
||||||
error: null,
|
error: null,
|
||||||
widgetUrl: u.format(),
|
widgetUrl: u.format(),
|
||||||
loading: false,
|
initialising: false,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
|
console.error("Failed to get scalar_token", err);
|
||||||
this.setState({
|
this.setState({
|
||||||
error: err.message,
|
error: err.message,
|
||||||
loading: false,
|
initialising: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
window.addEventListener('message', this._onMessage, false);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
WidgetMessaging.stopListening();
|
||||||
|
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
||||||
window.removeEventListener('message', this._onMessage);
|
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) {
|
_onMessage(event) {
|
||||||
if (this.props.type !== 'jitsi') {
|
if (this.props.type !== 'jitsi') {
|
||||||
return;
|
return;
|
||||||
|
@ -154,11 +241,11 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_canUserModify: function() {
|
_canUserModify() {
|
||||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||||
},
|
},
|
||||||
|
|
||||||
_onEditClick: function(e) {
|
_onEditClick(e) {
|
||||||
console.log("Edit widget ID ", this.props.id);
|
console.log("Edit widget ID ", this.props.id);
|
||||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||||
|
@ -168,9 +255,10 @@ export default React.createClass({
|
||||||
}, "mx_IntegrationsManager");
|
}, "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()) {
|
if (this._canUserModify()) {
|
||||||
// Show delete confirmation dialog
|
// Show delete confirmation dialog
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
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
|
// 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
|
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||||
_deleteWidgetLabel() {
|
_deleteWidgetLabel() {
|
||||||
|
@ -224,7 +329,7 @@ export default React.createClass({
|
||||||
this.setState({hasPermissionToLoad: false});
|
this.setState({hasPermissionToLoad: false});
|
||||||
},
|
},
|
||||||
|
|
||||||
formatAppTileName: function() {
|
formatAppTileName() {
|
||||||
let appTileName = "No name";
|
let appTileName = "No name";
|
||||||
if (this.props.name && this.props.name.trim()) {
|
if (this.props.name && this.props.name.trim()) {
|
||||||
appTileName = this.props.name.trim();
|
appTileName = this.props.name.trim();
|
||||||
|
@ -232,7 +337,7 @@ export default React.createClass({
|
||||||
return appTileName;
|
return appTileName;
|
||||||
},
|
},
|
||||||
|
|
||||||
onClickMenuBar: function(ev) {
|
onClickMenuBar(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
// Ignore clicks on menu bar children
|
// 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;
|
let appTileBody;
|
||||||
|
|
||||||
// Don't render widget if it is in the process of being deleted
|
// 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.
|
// a link to it.
|
||||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||||
"allow-same-origin allow-scripts allow-presentation";
|
"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.props.show) {
|
||||||
if (this.state.loading) {
|
const loadingElement = (
|
||||||
appTileBody = (
|
|
||||||
<div className='mx_AppTileBody mx_AppLoading'>
|
<div className='mx_AppTileBody mx_AppLoading'>
|
||||||
<MessageSpinner msg='Loading...' />
|
<MessageSpinner msg='Loading...' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
if (this.state.initialising) {
|
||||||
|
appTileBody = loadingElement;
|
||||||
} else if (this.state.hasPermissionToLoad == true) {
|
} else if (this.state.hasPermissionToLoad == true) {
|
||||||
if (this.isMixedContent()) {
|
if (this.isMixedContent()) {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className="mx_AppTileBody">
|
<div className="mx_AppTileBody">
|
||||||
<AppWarning
|
<AppWarning errorMsg="Error - Mixed content" />
|
||||||
errorMsg="Error - Mixed content"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className="mx_AppTileBody">
|
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
|
||||||
|
{ this.state.loading && loadingElement }
|
||||||
<iframe
|
<iframe
|
||||||
ref="appFrame"
|
ref="appFrame"
|
||||||
src={safeWidgetUrl}
|
src={this._getSafeUrl()}
|
||||||
allowFullScreen="true"
|
allowFullScreen="true"
|
||||||
sandbox={sandboxFlags}
|
sandbox={sandboxFlags}
|
||||||
|
onLoad={this._onLoaded}
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -313,8 +423,8 @@ export default React.createClass({
|
||||||
// editing is done in scalar
|
// editing is done in scalar
|
||||||
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
|
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
|
||||||
const deleteWidgetLabel = this._deleteWidgetLabel();
|
const deleteWidgetLabel = this._deleteWidgetLabel();
|
||||||
let deleteIcon = 'img/cancel.svg';
|
let deleteIcon = 'img/cancel_green.svg';
|
||||||
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
|
let deleteClasses = 'mx_AppTileMenuBarWidget';
|
||||||
if (this._canUserModify()) {
|
if (this._canUserModify()) {
|
||||||
deleteIcon = 'img/icon-delete-pink.svg';
|
deleteIcon = 'img/icon-delete-pink.svg';
|
||||||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||||
|
@ -323,25 +433,29 @@ export default React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||||
{ this.formatAppTileName() }
|
<b>{ this.formatAppTileName() }</b>
|
||||||
|
{ this.state.widgetPageTitle && (
|
||||||
|
<span> - { this.state.widgetPageTitle }</span>
|
||||||
|
) }
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
{ /* Edit widget */ }
|
{ /* Edit widget */ }
|
||||||
{ showEditButton && <img
|
{ showEditButton && <TintableSvgButton
|
||||||
src="img/edit.svg"
|
src="img/edit_green.svg"
|
||||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
width="8" height="8"
|
|
||||||
alt={_t('Edit')}
|
|
||||||
title={_t('Edit')}
|
title={_t('Edit')}
|
||||||
onClick={this._onEditClick}
|
onClick={this._onEditClick}
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
/> }
|
/> }
|
||||||
|
|
||||||
{ /* Delete widget */ }
|
{ /* Delete widget */ }
|
||||||
<img src={deleteIcon}
|
<TintableSvgButton
|
||||||
|
src={deleteIcon}
|
||||||
className={deleteClasses}
|
className={deleteClasses}
|
||||||
width="8" height="8"
|
|
||||||
alt={_t(deleteWidgetLabel)}
|
|
||||||
title={_t(deleteWidgetLabel)}
|
title={_t(deleteWidgetLabel)}
|
||||||
onClick={this._onDeleteClick}
|
onClick={this._onDeleteClick}
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {MatrixClient} from 'matrix-js-sdk';
|
import {MatrixClient} from 'matrix-js-sdk';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
|
@ -43,18 +42,22 @@ class FlairAvatar extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(
|
const httpUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||||
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
|
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
|
return <img
|
||||||
src={httpUrl}
|
src={httpUrl}
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
title={this.props.groupProfile.groupId} />;
|
title={tooltip} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FlairAvatar.propTypes = {
|
FlairAvatar.propTypes = {
|
||||||
groupProfile: PropTypes.shape({
|
groupProfile: PropTypes.shape({
|
||||||
groupId: PropTypes.string.isRequired,
|
groupId: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string,
|
||||||
avatarUrl: PropTypes.string.isRequired,
|
avatarUrl: PropTypes.string.isRequired,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -69,26 +72,19 @@ export default class Flair extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
profiles: [],
|
profiles: [],
|
||||||
};
|
};
|
||||||
this.onRoomStateEvents = this.onRoomStateEvents.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._unmounted = true;
|
this._unmounted = true;
|
||||||
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
if (UserSettingsStore.isFeatureEnabled('feature_groups') && FlairStore.groupSupport()) {
|
this._generateAvatars(this.props.groups);
|
||||||
this._generateAvatars();
|
|
||||||
}
|
|
||||||
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRoomStateEvents(event) {
|
componentWillReceiveProps(newProps) {
|
||||||
if (event.getType() === 'm.room.related_groups' && FlairStore.groupSupport()) {
|
this._generateAvatars(newProps.groups);
|
||||||
this._generateAvatars();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getGroupProfiles(groups) {
|
async _getGroupProfiles(groups) {
|
||||||
|
@ -105,23 +101,7 @@ export default class Flair extends React.Component {
|
||||||
return profiles.filter((p) => p !== null);
|
return profiles.filter((p) => p !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _generateAvatars() {
|
async _generateAvatars(groups) {
|
||||||
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 = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!groups || groups.length === 0) {
|
if (!groups || groups.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -147,13 +127,7 @@ export default class Flair extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
Flair.propTypes = {
|
Flair.propTypes = {
|
||||||
userId: PropTypes.string,
|
groups: PropTypes.arrayOf(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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using
|
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using
|
||||||
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
|
@ -18,8 +18,8 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
|
||||||
import * as languageHandler from '../../../languageHandler';
|
import * as languageHandler from '../../../languageHandler';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
function languageMatchesSearchQuery(query, language) {
|
function languageMatchesSearchQuery(query, language) {
|
||||||
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
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
|
// If no value is given, we start with the first
|
||||||
// country selected, but our parent component
|
// country selected, but our parent component
|
||||||
// doesn't know this, therefore we do this.
|
// doesn't know this, therefore we do this.
|
||||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||||
if (_localSettings.hasOwnProperty('language')) {
|
if (language) {
|
||||||
this.props.onOptionChange(_localSettings.language);
|
this.props.onOptionChange(language);
|
||||||
} else {
|
} else {
|
||||||
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
||||||
this.props.onOptionChange(language);
|
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
|
// default value here too, otherwise we need to handle null / undefined
|
||||||
// values between mounting and the initial value propgating
|
// values between mounting and the initial value propgating
|
||||||
|
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||||
let value = null;
|
let value = null;
|
||||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
if (language) {
|
||||||
if (_localSettings.hasOwnProperty('language')) {
|
value = this.props.value || language;
|
||||||
value = this.props.value || _localSettings.language;
|
|
||||||
} else {
|
} else {
|
||||||
const language = navigator.language || navigator.userLanguage;
|
language = navigator.language || navigator.userLanguage;
|
||||||
value = this.props.value || language;
|
value = this.props.value || language;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -304,7 +304,7 @@ module.exports = React.createClass({
|
||||||
return items[0];
|
return items[0];
|
||||||
} else if (remaining > 0) {
|
} else if (remaining > 0) {
|
||||||
items = items.slice(0, itemLimit);
|
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 {
|
} else {
|
||||||
const lastItem = items.pop();
|
const lastItem = items.pop();
|
||||||
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
|
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
|
||||||
|
|
|
@ -20,14 +20,16 @@ import React from 'react';
|
||||||
import * as Roles from '../../../Roles';
|
import * as Roles from '../../../Roles';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
let LEVEL_ROLE_MAP = {};
|
|
||||||
const reverseRoles = {};
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'PowerSelector',
|
displayName: 'PowerSelector',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
value: React.PropTypes.number.isRequired,
|
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
|
// if true, the <select/> should be a 'controlled' form element and updated by React
|
||||||
// to reflect the current value, rather than left freeform.
|
// to reflect the current value, rather than left freeform.
|
||||||
|
@ -43,78 +45,98 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
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() {
|
componentWillMount: function() {
|
||||||
LEVEL_ROLE_MAP = Roles.levelRoleMap();
|
this._initStateFromProps(this.props);
|
||||||
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) {
|
},
|
||||||
reverseRoles[LEVEL_ROLE_MAP[key]] = key;
|
|
||||||
|
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) {
|
onSelectChange: function(event) {
|
||||||
this.setState({ custom: event.target.value === "Custom" });
|
this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
|
||||||
if (event.target.value !== "Custom") {
|
if (event.target.value !== "SELECT_VALUE_CUSTOM") {
|
||||||
this.props.onChange(this.getValue());
|
this.props.onChange(event.target.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onCustomBlur: function(event) {
|
onCustomBlur: function(event) {
|
||||||
this.props.onChange(this.getValue());
|
this.props.onChange(parseInt(this.refs.custom.value));
|
||||||
},
|
},
|
||||||
|
|
||||||
onCustomKeyDown: function(event) {
|
onCustomKeyDown: function(event) {
|
||||||
if (event.key == "Enter") {
|
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() {
|
render: function() {
|
||||||
let customPicker;
|
let customPicker;
|
||||||
if (this.state.custom) {
|
if (this.state.custom) {
|
||||||
let input;
|
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
input = <span>{ this.props.value }</span>;
|
customPicker = <span>{ _t(
|
||||||
|
"Custom of %(powerLevel)s",
|
||||||
|
{ powerLevel: this.props.value },
|
||||||
|
) }</span>;
|
||||||
} else {
|
} 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;
|
let selectValue;
|
||||||
if (this.state.custom) {
|
if (this.state.custom) {
|
||||||
selectValue = "Custom";
|
selectValue = "SELECT_VALUE_CUSTOM";
|
||||||
} else {
|
} else {
|
||||||
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom";
|
selectValue = this.state.levelRoleMap[this.props.value] ?
|
||||||
|
this.props.value : "SELECT_VALUE_CUSTOM";
|
||||||
}
|
}
|
||||||
let select;
|
let select;
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
select = <span>{ selectValue }</span>;
|
select = <span>{ this.state.levelRoleMap[selectValue] }</span>;
|
||||||
} else {
|
} else {
|
||||||
// Each level must have a definition in LEVEL_ROLE_MAP
|
// Each level must have a definition in this.state.levelRoleMap
|
||||||
const levels = [0, 50, 100];
|
let options = this.state.options.map((level) => {
|
||||||
let options = levels.map((level) => {
|
|
||||||
return {
|
return {
|
||||||
value: LEVEL_ROLE_MAP[level],
|
value: level,
|
||||||
// Give a userDefault (users_default in the power event) of 0 but
|
text: Roles.textualPowerLevel(level, this.props.usersDefault),
|
||||||
// because level !== undefined, this should never be used.
|
|
||||||
text: Roles.textualPowerLevel(level, 0),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
options.push({ value: "Custom", text: _t("Custom level") });
|
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
|
||||||
options = options.map((op) => {
|
options = options.map((op) => {
|
||||||
return <option value={op.value} key={op.value}>{ op.text }</option>;
|
return <option value={op.value} key={op.value}>{ op.text }</option>;
|
||||||
});
|
});
|
||||||
|
|
110
src/components/views/elements/SettingsFlag.js
Normal file
110
src/components/views/elements/SettingsFlag.js
Normal 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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
61
src/components/views/elements/TintableSvgButton.js
Normal file
61
src/components/views/elements/TintableSvgButton.js
Normal 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() {},
|
||||||
|
};
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
@ -27,6 +28,10 @@ export default React.createClass({
|
||||||
group: PropTypes.object.isRequired,
|
group: PropTypes.object.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
contextTypes: {
|
||||||
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
|
},
|
||||||
|
|
||||||
onClick: function(e) {
|
onClick: function(e) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_group',
|
action: 'view_group',
|
||||||
|
@ -39,13 +44,15 @@ export default React.createClass({
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
|
|
||||||
const groupName = this.props.group.name || this.props.group.groupId;
|
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
|
const label = <EmojiText
|
||||||
element="div"
|
element="div"
|
||||||
title={this.props.group.groupId}
|
title={this.props.group.groupId}
|
||||||
className="mx_RoomTile_name"
|
className="mx_RoomTile_name mx_RoomTile_badgeShown"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
{ groupName }
|
{ groupName }
|
||||||
|
|
|
@ -59,9 +59,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_initGroupStore(groupId) {
|
_initGroupStore(groupId) {
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(
|
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
||||||
this.context.matrixClient, this.props.groupId,
|
|
||||||
);
|
|
||||||
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -83,6 +81,7 @@ module.exports = React.createClass({
|
||||||
_onKick: function() {
|
_onKick: function() {
|
||||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||||
Modal.createDialog(ConfirmUserActionDialog, {
|
Modal.createDialog(ConfirmUserActionDialog, {
|
||||||
|
matrixClient: this.context.matrixClient,
|
||||||
groupMember: this.props.groupMember,
|
groupMember: this.props.groupMember,
|
||||||
action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'),
|
action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'),
|
||||||
title: this.state.isUserInvited ? _t('Disinvite this user from community?')
|
title: this.state.isUserInvited ? _t('Disinvite this user from community?')
|
||||||
|
|
|
@ -20,17 +20,12 @@ import sdk from '../../../index';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
|
||||||
|
|
||||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||||
|
|
||||||
export default withMatrixClient(React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'GroupMemberList',
|
displayName: 'GroupMemberList',
|
||||||
|
|
||||||
contextTypes: {
|
|
||||||
matrixClient: PropTypes.object.isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
groupId: PropTypes.string.isRequired,
|
groupId: PropTypes.string.isRequired,
|
||||||
},
|
},
|
||||||
|
@ -49,7 +44,7 @@ export default withMatrixClient(React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_initGroupStore: function(groupId) {
|
_initGroupStore: function(groupId) {
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
|
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||||
this._groupStore.registerListener(() => {
|
this._groupStore.registerListener(() => {
|
||||||
this._fetchMembers();
|
this._fetchMembers();
|
||||||
});
|
});
|
||||||
|
@ -92,7 +87,7 @@ export default withMatrixClient(React.createClass({
|
||||||
query = (query || "").toLowerCase();
|
query = (query || "").toLowerCase();
|
||||||
if (query) {
|
if (query) {
|
||||||
memberList = memberList.filter((m) => {
|
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);
|
const matchesId = m.userId.toLowerCase().includes(query);
|
||||||
|
|
||||||
if (!matchesName && !matchesId) {
|
if (!matchesName && !matchesId) {
|
||||||
|
@ -108,15 +103,21 @@ export default withMatrixClient(React.createClass({
|
||||||
if (!uniqueMembers[m.userId]) uniqueMembers[m.userId] = m;
|
if (!uniqueMembers[m.userId]) uniqueMembers[m.userId] = m;
|
||||||
});
|
});
|
||||||
memberList = Object.keys(uniqueMembers).map((userId) => uniqueMembers[userId]);
|
memberList = Object.keys(uniqueMembers).map((userId) => uniqueMembers[userId]);
|
||||||
|
// Descending sort on isPrivileged = true = 1 to isPrivileged = false = 0
|
||||||
memberList.sort((a, b) => {
|
memberList.sort((a, b) => {
|
||||||
// TODO: should put admins at the top: we don't yet have that info
|
if (a.isPrivileged === b.isPrivileged) {
|
||||||
if (a < b) {
|
const aName = a.displayname || a.userId;
|
||||||
|
const bName = b.displayname || b.userId;
|
||||||
|
if (aName < bName) {
|
||||||
return -1;
|
return -1;
|
||||||
} else if (a > b) {
|
} else if (aName > bName) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return a.isPrivileged ? -1 : 1;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const memberTiles = memberList.map((m) => {
|
const memberTiles = memberList.map((m) => {
|
||||||
|
@ -168,4 +169,4 @@ export default withMatrixClient(React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}));
|
});
|
||||||
|
|
86
src/components/views/groups/GroupPublicityToggle.js
Normal file
86
src/components/views/groups/GroupPublicityToggle.js
Normal 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>;
|
||||||
|
},
|
||||||
|
});
|
|
@ -61,9 +61,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_initGroupStore(groupId) {
|
_initGroupStore(groupId) {
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(
|
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
||||||
this.context.matrixClient, this.props.groupId,
|
|
||||||
);
|
|
||||||
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -137,7 +135,7 @@ module.exports = React.createClass({
|
||||||
const groupId = this.props.groupId;
|
const groupId = this.props.groupId;
|
||||||
const roomId = this.props.groupRoomId;
|
const roomId = this.props.groupRoomId;
|
||||||
const roomName = this.state.groupRoom.displayname;
|
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);
|
console.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
|
||||||
|
|
|
@ -19,15 +19,10 @@ import sdk from '../../../index';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {MatrixClient} from 'matrix-js-sdk';
|
|
||||||
|
|
||||||
const INITIAL_LOAD_NUM_ROOMS = 30;
|
const INITIAL_LOAD_NUM_ROOMS = 30;
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
contextTypes: {
|
|
||||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
groupId: PropTypes.string.isRequired,
|
groupId: PropTypes.string.isRequired,
|
||||||
},
|
},
|
||||||
|
@ -46,7 +41,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_initGroupStore: function(groupId) {
|
_initGroupStore: function(groupId) {
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
|
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||||
this._groupStore.registerListener(() => {
|
this._groupStore.registerListener(() => {
|
||||||
this._fetchRooms();
|
this._fetchRooms();
|
||||||
});
|
});
|
||||||
|
@ -94,7 +89,7 @@ export default React.createClass({
|
||||||
let roomList = this.state.rooms;
|
let roomList = this.state.rooms;
|
||||||
if (query) {
|
if (query) {
|
||||||
roomList = roomList.filter((room) => {
|
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);
|
const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
|
||||||
return matchesName || matchesAlias;
|
return matchesName || matchesAlias;
|
||||||
});
|
});
|
||||||
|
|
93
src/components/views/groups/GroupTile.js
Normal file
93
src/components/views/groups/GroupTile.js
Normal 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;
|
89
src/components/views/groups/GroupUserSettings.js
Normal file
89
src/components/views/groups/GroupUserSettings.js
Normal 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>;
|
||||||
|
},
|
||||||
|
});
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
const DIV_ID = 'mx_recaptcha';
|
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)
|
// * jumping straight to a hosted captcha page (but we don't support that yet)
|
||||||
// * embedding the captcha in an iframe (if that works)
|
// * embedding the captcha in an iframe (if that works)
|
||||||
// * using a better captcha lib
|
// * 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>",
|
"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);
|
this.refs.recaptchaContainer.appendChild(warning);
|
||||||
} else {
|
} else {
|
||||||
const scriptTag = document.createElement('script');
|
const scriptTag = document.createElement('script');
|
||||||
|
|
|
@ -20,7 +20,7 @@ import url from 'url';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
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
|
/* This file contains a collection of components which are used by the
|
||||||
* InteractiveAuth to prompt the user to enter the information needed
|
* InteractiveAuth to prompt the user to enter the information needed
|
||||||
|
@ -256,7 +256,10 @@ export const EmailIdentityAuthEntry = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<p>{ _t("Please check your email to continue registration.") }</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -370,7 +373,10 @@ export const MsisdnAuthEntry = React.createClass({
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<p>{ _t("Please enter the code it contains:") }</p>
|
||||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||||
<form onSubmit={this._onFormSubmit}>
|
<form onSubmit={this._onFormSubmit}>
|
||||||
|
|
59
src/components/views/login/LoginPage.js
Normal file
59
src/components/views/login/LoginPage.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
|
@ -20,7 +20,7 @@ import classNames from 'classnames';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {field_input_incorrect} from '../../../UiEffects';
|
import {field_input_incorrect} from '../../../UiEffects';
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pure UI component which displays a username/password form.
|
* A pure UI component which displays a username/password form.
|
||||||
|
@ -144,7 +144,10 @@ class PasswordLogin extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
name="username" // make it a little easier for browser's remember-password
|
name="username" // make it a little easier for browser's remember-password
|
||||||
onChange={this.onUsernameChanged}
|
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}
|
value={this.state.username}
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -210,9 +213,9 @@ class PasswordLogin extends React.Component {
|
||||||
|
|
||||||
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
|
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
|
||||||
|
|
||||||
return (
|
let loginType;
|
||||||
<div>
|
if (!SdkConfig.get().disable_3pid_login) {
|
||||||
<form onSubmit={this.onSubmitForm}>
|
loginType = (
|
||||||
<div className="mx_Login_type_container">
|
<div className="mx_Login_type_container">
|
||||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
@ -225,6 +228,13 @@ class PasswordLogin extends React.Component {
|
||||||
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
|
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={this.onSubmitForm}>
|
||||||
|
{ loginType }
|
||||||
{ loginField }
|
{ loginField }
|
||||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
|
|
@ -22,6 +22,8 @@ import Email from '../../../email';
|
||||||
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
const FIELD_EMAIL = 'field_email';
|
const FIELD_EMAIL = 'field_email';
|
||||||
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||||
|
@ -122,7 +124,7 @@ module.exports = React.createClass({
|
||||||
password: this.refs.password.value.trim(),
|
password: this.refs.password.value.trim(),
|
||||||
email: email,
|
email: email,
|
||||||
phoneCountry: this.state.phoneCountry,
|
phoneCountry: this.state.phoneCountry,
|
||||||
phoneNumber: this.refs.phoneNumber.value.trim(),
|
phoneNumber: this.refs.phoneNumber ? this.refs.phoneNumber.value.trim() : '',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
|
@ -180,7 +182,7 @@ module.exports = React.createClass({
|
||||||
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||||
break;
|
break;
|
||||||
case FIELD_PHONE_NUMBER:
|
case FIELD_PHONE_NUMBER:
|
||||||
const phoneNumber = this.refs.phoneNumber.value;
|
const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : '';
|
||||||
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||||
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||||
break;
|
break;
|
||||||
|
@ -273,10 +275,14 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const self = this;
|
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 = (
|
const emailSection = (
|
||||||
<div>
|
<div>
|
||||||
<input type="text" ref="email"
|
<input type="text" ref="email"
|
||||||
autoFocus={true} placeholder={_t("Email address (optional)")}
|
autoFocus={true} placeholder={emailPlaceholder}
|
||||||
defaultValue={this.props.defaultEmail}
|
defaultValue={this.props.defaultEmail}
|
||||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||||
|
@ -306,7 +312,9 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||||
const phoneSection = (
|
let phoneSection;
|
||||||
|
if (!SdkConfig.get().disable_3pid_login) {
|
||||||
|
phoneSection = (
|
||||||
<div className="mx_Login_phoneSection">
|
<div className="mx_Login_phoneSection">
|
||||||
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||||
|
@ -328,6 +336,7 @@ module.exports = React.createClass({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const registerButton = (
|
const registerButton = (
|
||||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
|
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
|
||||||
|
|
|
@ -25,8 +25,8 @@ import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MImageBody',
|
displayName: 'MImageBody',
|
||||||
|
@ -81,7 +81,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onImageEnter: function(e) {
|
onImageEnter: function(e) {
|
||||||
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const imgElement = e.target;
|
const imgElement = e.target;
|
||||||
|
@ -89,7 +89,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onImageLeave: function(e) {
|
onImageLeave: function(e) {
|
||||||
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const imgElement = e.target;
|
const imgElement = e.target;
|
||||||
|
@ -218,7 +218,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const contentUrl = this._getContentUrl();
|
const contentUrl = this._getContentUrl();
|
||||||
let thumbUrl;
|
let thumbUrl;
|
||||||
if (this._isGif() && UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||||
thumbUrl = contentUrl;
|
thumbUrl = contentUrl;
|
||||||
} else {
|
} else {
|
||||||
thumbUrl = this._getThumbUrl();
|
thumbUrl = this._getThumbUrl();
|
||||||
|
@ -230,6 +230,7 @@ module.exports = React.createClass({
|
||||||
<a href={contentUrl} onClick={this.onClick}>
|
<a href={contentUrl} onClick={this.onClick}>
|
||||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||||
alt={content.body}
|
alt={content.body}
|
||||||
|
onLoad={this.props.onWidgetLoad}
|
||||||
onMouseEnter={this.onImageEnter}
|
onMouseEnter={this.onImageEnter}
|
||||||
onMouseLeave={this.onImageLeave} />
|
onMouseLeave={this.onImageLeave} />
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -21,8 +21,8 @@ import MFileBody from './MFileBody';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MVideoBody',
|
displayName: 'MVideoBody',
|
||||||
|
@ -151,7 +151,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const contentUrl = this._getContentUrl();
|
const contentUrl = this._getContentUrl();
|
||||||
const thumbUrl = this._getThumbUrl();
|
const thumbUrl = this._getThumbUrl();
|
||||||
const autoplay = UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false);
|
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
|
||||||
let height = null;
|
let height = null;
|
||||||
let width = null;
|
let width = null;
|
||||||
let poster = null;
|
let poster = null;
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { ContentRepo } from 'matrix-js-sdk';
|
import { ContentRepo } from 'matrix-js-sdk';
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
@ -67,24 +67,17 @@ module.exports = React.createClass({
|
||||||
'crop',
|
'crop',
|
||||||
);
|
);
|
||||||
|
|
||||||
// it sucks that _tJsx doesn't support normal _t substitutions :((
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomAvatarEvent">
|
<div className="mx_RoomAvatarEvent">
|
||||||
{ _tJsx('%(senderDisplayName)s changed the room avatar to <img/>',
|
{ _t('%(senderDisplayName)s changed the room avatar to <img/>',
|
||||||
[
|
{ senderDisplayName: senderDisplayName },
|
||||||
/%\(senderDisplayName\)s/,
|
{
|
||||||
/<img\/>/,
|
'img': () =>
|
||||||
],
|
|
||||||
[
|
|
||||||
(sub) => senderDisplayName,
|
|
||||||
(sub) =>
|
|
||||||
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
|
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
|
||||||
onClick={this.onAvatarClick.bind(this, name)}>
|
onClick={this.onAvatarClick.bind(this, name)}>
|
||||||
<BaseAvatar width={14} height={14} url={url}
|
<BaseAvatar width={14} height={14} url={url} name={name} />
|
||||||
name={name} />
|
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
],
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,53 +17,128 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {MatrixClient} from 'matrix-js-sdk';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Flair from '../elements/Flair.js';
|
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 EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
const {mxEvent} = props;
|
const {mxEvent} = this.props;
|
||||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
let name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||||
const {msgtype} = mxEvent.getContent();
|
const {msgtype} = mxEvent.getContent();
|
||||||
|
|
||||||
if (msgtype === 'm.emote') {
|
if (msgtype === 'm.emote') {
|
||||||
return <span />; // emote message must include the name so don't duplicate it
|
return <span />; // emote message must include the name so don't duplicate it
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name + flair
|
let flair = <div />;
|
||||||
const nameElem = [
|
if (this.props.enableFlair) {
|
||||||
<EmojiText key='name' className="mx_SenderProfile_name">{ name || '' }</EmojiText>,
|
const displayedGroups = this._getDisplayedGroups(
|
||||||
props.enableFlair ?
|
this.state.userGroups, this.state.relatedGroups,
|
||||||
<Flair key='flair'
|
);
|
||||||
|
|
||||||
|
// Backwards-compatible replacing of "(IRC)" with AS user flair
|
||||||
|
name = displayedGroups.length > 0 ? name.replace(' (IRC)', '') : name;
|
||||||
|
|
||||||
|
flair = <Flair key='flair'
|
||||||
userId={mxEvent.getSender()}
|
userId={mxEvent.getSender()}
|
||||||
roomId={mxEvent.getRoomId()}
|
groups={displayedGroups}
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
|
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
|
||||||
{ content }
|
{ content }
|
||||||
</div>
|
</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,
|
|
||||||
};
|
|
||||||
|
|
|
@ -29,11 +29,11 @@ import Modal from '../../../Modal';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import UserSettingsStore from "../../../UserSettingsStore";
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import ContextualMenu from '../../structures/ContextualMenu';
|
import ContextualMenu from '../../structures/ContextualMenu';
|
||||||
import {RoomMember} from 'matrix-js-sdk';
|
import {RoomMember} from 'matrix-js-sdk';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
@ -104,7 +104,7 @@ module.exports = React.createClass({
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this._unmounted) return;
|
if (this._unmounted) return;
|
||||||
for (let i = 0; i < blocks.length; i++) {
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
|
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||||
highlight.highlightBlock(blocks[i]);
|
highlight.highlightBlock(blocks[i]);
|
||||||
} else {
|
} else {
|
||||||
// Only syntax highlight if there's a class starting with language-
|
// Only syntax highlight if there's a class starting with language-
|
||||||
|
@ -169,7 +169,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
pillifyLinks: function(nodes) {
|
pillifyLinks: function(nodes) {
|
||||||
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
||||||
let node = nodes[0];
|
let node = nodes[0];
|
||||||
while (node) {
|
while (node) {
|
||||||
let pillified = false;
|
let pillified = false;
|
||||||
|
@ -224,7 +224,7 @@ module.exports = React.createClass({
|
||||||
if (roomNotifTextNodes.length > 0) {
|
if (roomNotifTextNodes.length > 0) {
|
||||||
const pushProcessor = new PushProcessor(MatrixClientPeg.get());
|
const pushProcessor = new PushProcessor(MatrixClientPeg.get());
|
||||||
const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif");
|
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
|
// Now replace all those nodes with Pills
|
||||||
for (const roomNotifTextNode of roomNotifTextNodes) {
|
for (const roomNotifTextNode of roomNotifTextNodes) {
|
||||||
const pillContainer = document.createElement('span');
|
const pillContainer = document.createElement('span');
|
||||||
|
@ -419,7 +419,7 @@ module.exports = React.createClass({
|
||||||
const content = mxEvent.getContent();
|
const content = mxEvent.getContent();
|
||||||
|
|
||||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||||
disableBigEmoji: UserSettingsStore.getSyncedSetting('TextualBody.disableBigEmoji', false),
|
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.props.highlightLink) {
|
if (this.props.highlightLink) {
|
||||||
|
|
|
@ -22,10 +22,11 @@ const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
const Modal = require("../../../Modal");
|
const Modal = require("../../../Modal");
|
||||||
|
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
const ROOM_COLORS = [
|
const ROOM_COLORS = [
|
||||||
// magic room default values courtesy of Ribot
|
// magic room default values courtesy of Ribot
|
||||||
["#76cfa6", "#eaf5f0"],
|
[Tinter.getKeyRgb()[0], Tinter.getKeyRgb()[1]],
|
||||||
["#81bddb", "#eaf1f4"],
|
["#81bddb", "#eaf1f4"],
|
||||||
["#bd79cb", "#f3eaf5"],
|
["#bd79cb", "#f3eaf5"],
|
||||||
["#c65d94", "#f5eaef"],
|
["#c65d94", "#f5eaef"],
|
||||||
|
@ -47,17 +48,17 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
const data = {
|
const data = {
|
||||||
index: 0,
|
index: 0,
|
||||||
primary_color: ROOM_COLORS[0].primary_color,
|
primary_color: ROOM_COLORS[0][0],
|
||||||
secondary_color: ROOM_COLORS[0].secondary_color,
|
secondary_color: ROOM_COLORS[0][1],
|
||||||
hasChanged: false,
|
hasChanged: false,
|
||||||
};
|
};
|
||||||
const event = this.props.room.getAccountData("org.matrix.room.color_scheme");
|
const scheme = SettingsStore.getValueAt(SettingLevel.ROOM_ACCOUNT, "roomColor", this.props.room.roomId);
|
||||||
if (!event) {
|
|
||||||
return data;
|
if (scheme.primary_color && scheme.secondary_color) {
|
||||||
}
|
// We only use the user's scheme if the scheme is valid.
|
||||||
const scheme = event.getContent();
|
|
||||||
data.primary_color = scheme.primary_color;
|
data.primary_color = scheme.primary_color;
|
||||||
data.secondary_color = scheme.secondary_color;
|
data.secondary_color = scheme.secondary_color;
|
||||||
|
}
|
||||||
data.index = this._getColorIndex(data);
|
data.index = this._getColorIndex(data);
|
||||||
|
|
||||||
if (data.index === -1) {
|
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
|
// 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
|
// they can't, so we still send the request but display a sensible
|
||||||
// error if it fails.
|
// error if it fails.
|
||||||
return MatrixClientPeg.get().setRoomAccountData(
|
// TODO: Support guests for room color. Technically this is possible via granular settings
|
||||||
this.props.room.roomId, "org.matrix.room.color_scheme", {
|
// 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,
|
primary_color: this.state.primary_color,
|
||||||
secondary_color: this.state.secondary_color,
|
secondary_color: this.state.secondary_color,
|
||||||
},
|
}).catch(function(err) {
|
||||||
).catch(function(err) {
|
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||||
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
|
|
||||||
dis.dispatch({action: 'view_set_mxid'});
|
dis.dispatch({action: 'view_set_mxid'});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -104,8 +104,8 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const localDomain = this.context.matrixClient.getDomain();
|
const localDomain = this.context.matrixClient.getDomain();
|
||||||
const EditableItemList = sdk.getComponent('elements.EditableItemList');
|
const EditableItemList = sdk.getComponent('elements.EditableItemList');
|
||||||
return (<div>
|
return <div>
|
||||||
<h3>{ _t('Related Communities') }</h3>
|
<h3>{ _t('Flair') }</h3>
|
||||||
<EditableItemList
|
<EditableItemList
|
||||||
items={this.state.newGroupsList}
|
items={this.state.newGroupsList}
|
||||||
className={"mx_RelatedGroupSettings"}
|
className={"mx_RelatedGroupSettings"}
|
||||||
|
@ -115,12 +115,12 @@ module.exports = React.createClass({
|
||||||
onItemAdded={this.onGroupAdded}
|
onItemAdded={this.onGroupAdded}
|
||||||
onItemEdited={this.onGroupEdited}
|
onItemEdited={this.onGroupEdited}
|
||||||
onItemRemoved={this.onGroupDeleted}
|
onItemRemoved={this.onGroupDeleted}
|
||||||
itemsLabel={_t('Related communities for this room:')}
|
itemsLabel={_t('Showing flair for these communities:')}
|
||||||
noItemsLabel={_t('This room has no related communities')}
|
noItemsLabel={_t('This room is not showing flair for any communities')}
|
||||||
placeholder={_t(
|
placeholder={_t(
|
||||||
'New community ID (e.g. +foo:%(localDomain)s)', {localDomain},
|
'New community ID (e.g. +foo:%(localDomain)s)', {localDomain},
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>);
|
</div>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Travis Ralston
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Promise from 'bluebird';
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
|
||||||
const sdk = require("../../../index");
|
const sdk = require("../../../index");
|
||||||
const Modal = require("../../../Modal");
|
import { _t, _td } from '../../../languageHandler';
|
||||||
const UserSettingsStore = require('../../../UserSettingsStore');
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -30,137 +28,64 @@ module.exports = React.createClass({
|
||||||
room: React.PropTypes.object,
|
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() {
|
saveSettings: function() {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
|
||||||
if (this.state.globalDisableUrlPreview !== this.originalState.globalDisableUrlPreview) {
|
if (this.refs.urlPreviewsSelf) promises.push(this.refs.urlPreviewsSelf.save());
|
||||||
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));
|
|
||||||
|
|
||||||
return promises;
|
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() {
|
render: function() {
|
||||||
const self = this;
|
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||||
const roomState = this.props.room.currentState;
|
const roomId = this.props.room.roomId;
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
const maySetRoomPreviewUrls = roomState.mayClientSendStateEvent('org.matrix.room.preview_urls', cli);
|
let previewsForAccount = null;
|
||||||
let disableRoomPreviewUrls;
|
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
|
||||||
if (maySetRoomPreviewUrls) {
|
previewsForAccount = (
|
||||||
disableRoomPreviewUrls =
|
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||||
<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>)
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
urlPreviewText = (
|
previewsForAccount = (
|
||||||
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
|
_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 (
|
return (
|
||||||
<div className="mx_RoomSettings_toggles">
|
<div className="mx_RoomSettings_toggles">
|
||||||
<h3>{ _t("URL Previews") }</h3>
|
<h3>{ _t("URL Previews") }</h3>
|
||||||
|
|
||||||
<label>
|
<label>{ previewsForAccount }</label>
|
||||||
{ urlPreviewText }
|
{ previewsForRoom }
|
||||||
</label>
|
<label>{ previewsForRoomAccount }</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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,9 +24,10 @@ import isEqual from 'lodash/isEqual';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
|
||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
|
||||||
|
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import Autocompleter from '../../../autocomplete/Autocompleter';
|
import Autocompleter from '../../../autocomplete/Autocompleter';
|
||||||
|
|
||||||
const COMPOSER_SELECTED = 0;
|
const COMPOSER_SELECTED = 0;
|
||||||
|
@ -95,7 +96,7 @@ export default class Autocomplete extends React.Component {
|
||||||
});
|
});
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
|
let autocompleteDelay = SettingsStore.getValue("autocompleteDelay");
|
||||||
|
|
||||||
// Don't debounce if we are already showing completions
|
// Don't debounce if we are already showing completions
|
||||||
if (this.state.completions.length > 0 || this.state.forceComplete) {
|
if (this.state.completions.length > 0 || this.state.forceComplete) {
|
||||||
|
|
|
@ -21,8 +21,7 @@ import sdk from '../../../index';
|
||||||
import dis from "../../../dispatcher";
|
import dis from "../../../dispatcher";
|
||||||
import ObjectUtils from '../../../ObjectUtils';
|
import ObjectUtils from '../../../ObjectUtils';
|
||||||
import AppsDrawer from './AppsDrawer';
|
import AppsDrawer from './AppsDrawer';
|
||||||
import { _t, _tJsx} from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -100,13 +99,13 @@ module.exports = React.createClass({
|
||||||
supportedText = _t(" (unsupported)");
|
supportedText = _t(" (unsupported)");
|
||||||
} else {
|
} else {
|
||||||
joinNode = (<span>
|
joinNode = (<span>
|
||||||
{ _tJsx(
|
{ _t(
|
||||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||||
[/<voiceText>(.*?)<\/voiceText>/, /<videoText>(.*?)<\/videoText>/],
|
{},
|
||||||
[
|
{
|
||||||
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
|
'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
|
||||||
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
|
'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
|
||||||
],
|
},
|
||||||
) }
|
) }
|
||||||
</span>);
|
</span>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,22 +33,30 @@ const ObjectUtils = require('../../../ObjectUtils');
|
||||||
|
|
||||||
const eventTileTypes = {
|
const eventTileTypes = {
|
||||||
'm.room.message': 'messages.MessageEvent',
|
'm.room.message': 'messages.MessageEvent',
|
||||||
'm.room.member': 'messages.TextualEvent',
|
|
||||||
'm.call.invite': 'messages.TextualEvent',
|
'm.call.invite': 'messages.TextualEvent',
|
||||||
'm.call.answer': 'messages.TextualEvent',
|
'm.call.answer': 'messages.TextualEvent',
|
||||||
'm.call.hangup': 'messages.TextualEvent',
|
'm.call.hangup': 'messages.TextualEvent',
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateEventTileTypes = {
|
||||||
|
'm.room.member': 'messages.TextualEvent',
|
||||||
'm.room.name': 'messages.TextualEvent',
|
'm.room.name': 'messages.TextualEvent',
|
||||||
'm.room.avatar': 'messages.RoomAvatarEvent',
|
'm.room.avatar': 'messages.RoomAvatarEvent',
|
||||||
'm.room.topic': 'messages.TextualEvent',
|
|
||||||
'm.room.third_party_invite': 'messages.TextualEvent',
|
'm.room.third_party_invite': 'messages.TextualEvent',
|
||||||
'm.room.history_visibility': 'messages.TextualEvent',
|
'm.room.history_visibility': 'messages.TextualEvent',
|
||||||
'm.room.encryption': 'messages.TextualEvent',
|
'm.room.encryption': 'messages.TextualEvent',
|
||||||
|
'm.room.topic': 'messages.TextualEvent',
|
||||||
'm.room.power_levels': 'messages.TextualEvent',
|
'm.room.power_levels': 'messages.TextualEvent',
|
||||||
'm.room.pinned_events': 'messages.TextualEvent',
|
'm.room.pinned_events': 'messages.TextualEvent',
|
||||||
|
|
||||||
'im.vector.modular.widgets': '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;
|
const MAX_READ_AVATARS = 5;
|
||||||
|
|
||||||
// Our component structure for EventTiles on the timeline is:
|
// 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
|
// Info messages are basically information about commands processed on a room
|
||||||
const isInfoMessage = (eventType !== 'm.room.message');
|
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
|
// This shouldn't happen: the caller should check we support this type
|
||||||
// before trying to instantiate us
|
// before trying to instantiate us
|
||||||
if (!EventTileType) {
|
if (!EventTileType) {
|
||||||
|
@ -600,8 +608,10 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
module.exports.haveTileForEvent = function(e) {
|
module.exports.haveTileForEvent = function(e) {
|
||||||
// Only messages have a tile (black-rectangle) if redacted
|
// Only messages have a tile (black-rectangle) if redacted
|
||||||
if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
|
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) !== '';
|
return TextForEvent.textForEvent(e) !== '';
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import KeyCode from '../../../KeyCode';
|
import { KeyCode } from '../../../Keyboard';
|
||||||
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
|
|
@ -494,7 +494,6 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
const defaultPerms = {
|
const defaultPerms = {
|
||||||
can: {},
|
can: {},
|
||||||
muted: false,
|
muted: false,
|
||||||
modifyLevel: false,
|
|
||||||
};
|
};
|
||||||
const room = this.props.matrixClient.getRoom(member.roomId);
|
const room = this.props.matrixClient.getRoom(member.roomId);
|
||||||
if (!room) return defaultPerms;
|
if (!room) return defaultPerms;
|
||||||
|
@ -516,13 +515,15 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_calculateCanPermissions: function(me, them, powerLevels) {
|
_calculateCanPermissions: function(me, them, powerLevels) {
|
||||||
|
const isMe = me.userId === them.userId;
|
||||||
const can = {
|
const can = {
|
||||||
kick: false,
|
kick: false,
|
||||||
ban: false,
|
ban: false,
|
||||||
mute: false,
|
mute: false,
|
||||||
modifyLevel: false,
|
modifyLevel: false,
|
||||||
|
modifyLevelMax: 0,
|
||||||
};
|
};
|
||||||
const canAffectUser = them.powerLevel < me.powerLevel;
|
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
|
||||||
if (!canAffectUser) {
|
if (!canAffectUser) {
|
||||||
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
|
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
|
||||||
return can;
|
return can;
|
||||||
|
@ -531,16 +532,13 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||||
powerLevels.state_default
|
powerLevels.state_default
|
||||||
);
|
);
|
||||||
const levelToSend = (
|
|
||||||
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
|
|
||||||
powerLevels.events_default
|
|
||||||
);
|
|
||||||
|
|
||||||
can.kick = me.powerLevel >= powerLevels.kick;
|
can.kick = me.powerLevel >= powerLevels.kick;
|
||||||
can.ban = me.powerLevel >= powerLevels.ban;
|
can.ban = me.powerLevel >= powerLevels.ban;
|
||||||
can.mute = me.powerLevel >= editPowerLevel;
|
can.mute = me.powerLevel >= editPowerLevel;
|
||||||
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
|
can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
|
||||||
can.modifyLevel = me.powerLevel > them.powerLevel && me.powerLevel >= editPowerLevel;
|
can.modifyLevelMax = me.powerLevel;
|
||||||
|
|
||||||
return can;
|
return can;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -832,8 +830,11 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
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
|
if (this.props.member.roomId) { // is in room
|
||||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||||
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
|
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
|
||||||
|
@ -842,7 +843,9 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
{ _t("Level:") } <b>
|
{ _t("Level:") } <b>
|
||||||
<PowerSelector controlled={true}
|
<PowerSelector controlled={true}
|
||||||
value={parseInt(this.props.member.powerLevel)}
|
value={parseInt(this.props.member.powerLevel)}
|
||||||
|
maxValue={this.state.can.modifyLevelMax}
|
||||||
disabled={!this.state.can.modifyLevel}
|
disabled={!this.state.can.modifyLevel}
|
||||||
|
usersDefault={powerLevelUsersDefault}
|
||||||
onChange={this.onPowerChange} />
|
onChange={this.onPowerChange} />
|
||||||
</b>
|
</b>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,7 +22,7 @@ import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
|
|
||||||
export default class MessageComposer extends React.Component {
|
export default class MessageComposer extends React.Component {
|
||||||
|
@ -49,10 +49,10 @@ export default class MessageComposer extends React.Component {
|
||||||
inputState: {
|
inputState: {
|
||||||
style: [],
|
style: [],
|
||||||
blockType: null,
|
blockType: null,
|
||||||
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false),
|
isRichtextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
|
||||||
wordCount: 0,
|
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() {
|
onToggleFormattingClicked() {
|
||||||
UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting);
|
SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting);
|
||||||
this.setState({showFormatting: !this.state.showFormatting});
|
this.setState({showFormatting: !this.state.showFormatting});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +238,7 @@ export default class MessageComposer extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
const uploadInputStyle = {display: 'none'};
|
const uploadInputStyle = {display: 'none'};
|
||||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
const MemberPresenceAvatar = sdk.getComponent('avatars.MemberPresenceAvatar');
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
|
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
|
||||||
|
|
||||||
|
@ -246,7 +246,7 @@ export default class MessageComposer extends React.Component {
|
||||||
|
|
||||||
controls.push(
|
controls.push(
|
||||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||||
<MemberAvatar member={me} width={24} height={24} />
|
<MemberPresenceAvatar member={me} width={24} height={24} />
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -28,14 +28,13 @@ import Promise from 'bluebird';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||||
import SlashCommands from '../../../SlashCommands';
|
import SlashCommands from '../../../SlashCommands';
|
||||||
import KeyCode from '../../../KeyCode';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import Analytics from '../../../Analytics';
|
import Analytics from '../../../Analytics';
|
||||||
|
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
|
||||||
|
|
||||||
import * as RichText from '../../../RichText';
|
import * as RichText from '../../../RichText';
|
||||||
import * as HtmlUtils from '../../../HtmlUtils';
|
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');
|
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
|
||||||
|
|
||||||
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
|
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
|
||||||
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
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 {
|
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
|
||||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||||
let ctrlCmdOnly;
|
|
||||||
if (isMac) {
|
|
||||||
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
|
||||||
} else {
|
|
||||||
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
|
// 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
|
// 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.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
||||||
this.onTextPasted = this.onTextPasted.bind(this);
|
this.onTextPasted = this.onTextPasted.bind(this);
|
||||||
|
|
||||||
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
|
const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
|
||||||
|
|
||||||
Analytics.setRichtextMode(isRichtextEnabled);
|
Analytics.setRichtextMode(isRichtextEnabled);
|
||||||
|
|
||||||
|
@ -216,7 +210,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
|
createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
|
||||||
const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
|
const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
|
||||||
RichText.getScopedMDDecorators(this.props);
|
RichText.getScopedMDDecorators(this.props);
|
||||||
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
||||||
decorators.push({
|
decorators.push({
|
||||||
strategy: this.findPillEntities.bind(this),
|
strategy: this.findPillEntities.bind(this),
|
||||||
component: (entityProps) => {
|
component: (entityProps) => {
|
||||||
|
@ -384,7 +378,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTyping(isTyping) {
|
sendTyping(isTyping) {
|
||||||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
if (SettingsStore.getValue('dontSendTypingNotifications')) return;
|
||||||
MatrixClientPeg.get().sendTyping(
|
MatrixClientPeg.get().sendTyping(
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
this.isTyping, TYPING_SERVER_TIMEOUT,
|
this.isTyping, TYPING_SERVER_TIMEOUT,
|
||||||
|
@ -431,7 +425,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatic replacement of plaintext emoji to Unicode emoji
|
// 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
|
// The first matched group includes just the matched plaintext emoji
|
||||||
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||||
if (emojiMatch) {
|
if (emojiMatch) {
|
||||||
|
@ -551,7 +545,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
editorState: this.createEditorState(enabled, contentState),
|
editorState: this.createEditorState(enabled, contentState),
|
||||||
isRichtextEnabled: enabled,
|
isRichtextEnabled: enabled,
|
||||||
});
|
});
|
||||||
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled);
|
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyCommand = (command: string): boolean => {
|
handleKeyCommand = (command: string): boolean => {
|
||||||
|
|
|
@ -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) {
|
getDuration: function(time) {
|
||||||
if (!time) return;
|
if (!time) return;
|
||||||
const t = parseInt(time / 1000);
|
const t = parseInt(time / 1000);
|
||||||
|
@ -53,41 +55,39 @@ module.exports = React.createClass({
|
||||||
const d = parseInt(t / (60 * 60 * 24));
|
const d = parseInt(t / (60 * 60 * 24));
|
||||||
if (t < 60) {
|
if (t < 60) {
|
||||||
if (t < 0) {
|
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) {
|
if (t < 60 * 60) {
|
||||||
return _t("for %(amount)sm", {amount: m});
|
return _t("%(duration)sm", {duration: m});
|
||||||
}
|
}
|
||||||
if (t < 24 * 60 * 60) {
|
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 === "online") return _t("Online");
|
||||||
if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
|
if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
|
||||||
if (presence === "offline") return _t("Offline");
|
if (presence === "offline") return _t("Offline");
|
||||||
return _t("Unknown");
|
return _t("Unknown");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
if (this.props.activeAgo >= 0) {
|
|
||||||
const duration = this.getDuration(this.props.activeAgo);
|
|
||||||
const ago = this.props.currentlyActive || !duration ? "" : duration;
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_PresenceLabel">
|
<div className="mx_PresenceLabel">
|
||||||
{ this.getPrettyPresence(this.props.presenceState) } { ago }
|
{ this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className="mx_PresenceLabel">
|
|
||||||
{ this.getPrettyPresence(this.props.presenceState) }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,7 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import ManageIntegsButton from '../elements/ManageIntegsButton';
|
import ManageIntegsButton from '../elements/ManageIntegsButton';
|
||||||
import {CancelButton} from './SimpleRoomHeader';
|
import {CancelButton} from './SimpleRoomHeader';
|
||||||
import UserSettingsStore from "../../../UserSettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -339,7 +339,7 @@ module.exports = React.createClass({
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) {
|
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
|
||||||
let pinsIndicator = null;
|
let pinsIndicator = null;
|
||||||
if (this._hasUnreadPins()) {
|
if (this._hasUnreadPins()) {
|
||||||
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
|
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
|
||||||
|
|
|
@ -18,18 +18,18 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
const ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
const CallHandler = require('../../../CallHandler');
|
const CallHandler = require('../../../CallHandler');
|
||||||
const RoomListSorter = require("../../../RoomListSorter");
|
|
||||||
const Unread = require('../../../Unread');
|
|
||||||
const dis = require("../../../dispatcher");
|
const dis = require("../../../dispatcher");
|
||||||
const sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
const rate_limited_func = require('../../../ratelimitedfunc');
|
const rate_limited_func = require('../../../ratelimitedfunc');
|
||||||
const Rooms = require('../../../Rooms');
|
const Rooms = require('../../../Rooms');
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
const Receipt = require('../../../utils/Receipt');
|
const Receipt = require('../../../utils/Receipt');
|
||||||
|
import FilterStore from '../../../stores/FilterStore';
|
||||||
|
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||||
|
|
||||||
const HIDE_CONFERENCE_CHANS = true;
|
const HIDE_CONFERENCE_CHANS = true;
|
||||||
|
|
||||||
|
@ -63,6 +63,7 @@ module.exports = React.createClass({
|
||||||
totalRoomCount: null,
|
totalRoomCount: null,
|
||||||
lists: {},
|
lists: {},
|
||||||
incomingCall: null,
|
incomingCall: null,
|
||||||
|
selectedTags: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -82,6 +83,23 @@ module.exports = React.createClass({
|
||||||
cli.on("accountData", this.onAccountData);
|
cli.on("accountData", this.onAccountData);
|
||||||
cli.on("Group.myMembership", this._onGroupMyMembership);
|
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();
|
this.refreshRoomList();
|
||||||
|
|
||||||
// order of the sublists
|
// order of the sublists
|
||||||
|
@ -150,6 +168,11 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._filterStoreToken) {
|
||||||
|
this._filterStoreToken.remove();
|
||||||
|
}
|
||||||
|
|
||||||
// cancel any pending calls to the rate_limited_funcs
|
// cancel any pending calls to the rate_limited_funcs
|
||||||
this._delayedRefreshRoomList.cancelPendingCall();
|
this._delayedRefreshRoomList.cancelPendingCall();
|
||||||
},
|
},
|
||||||
|
@ -236,6 +259,41 @@ module.exports = React.createClass({
|
||||||
this.refreshRoomList();
|
this.refreshRoomList();
|
||||||
}, 500),
|
}, 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() {
|
refreshRoomList: function() {
|
||||||
// TODO: ideally we'd calculate this once at start, and then maintain
|
// TODO: ideally we'd calculate this once at start, and then maintain
|
||||||
// any changes to it incrementally, updating the appropriate sublists
|
// any changes to it incrementally, updating the appropriate sublists
|
||||||
|
@ -255,9 +313,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getRoomLists: function() {
|
getRoomLists: function() {
|
||||||
const self = this;
|
|
||||||
const lists = {};
|
const lists = {};
|
||||||
|
|
||||||
lists["im.vector.fake.invite"] = [];
|
lists["im.vector.fake.invite"] = [];
|
||||||
lists["m.favourite"] = [];
|
lists["m.favourite"] = [];
|
||||||
lists["im.vector.fake.recent"] = [];
|
lists["im.vector.fake.recent"] = [];
|
||||||
|
@ -266,8 +322,7 @@ module.exports = React.createClass({
|
||||||
lists["im.vector.fake.archived"] = [];
|
lists["im.vector.fake.archived"] = [];
|
||||||
|
|
||||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||||
|
MatrixClientPeg.get().getRooms().forEach((room) => {
|
||||||
MatrixClientPeg.get().getRooms().forEach(function(room) {
|
|
||||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
|
@ -278,13 +333,18 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (me.membership == "invite") {
|
if (me.membership == "invite") {
|
||||||
lists["im.vector.fake.invite"].push(room);
|
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
|
// skip past this room & don't put it in any lists
|
||||||
} else if (me.membership == "join" || me.membership === "ban" ||
|
} else if (me.membership == "join" || me.membership === "ban" ||
|
||||||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
|
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
|
||||||
// Used to split rooms via tags
|
// Used to split rooms via tags
|
||||||
const tagNames = Object.keys(room.tags);
|
const tagNames = Object.keys(room.tags);
|
||||||
|
|
||||||
|
// Apply TagPanel filtering, derived from FilterStore
|
||||||
|
if (!this.isRoomInSelectedTags(room, me, dmRoomMap)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (tagNames.length) {
|
if (tagNames.length) {
|
||||||
for (let i = 0; i < tagNames.length; i++) {
|
for (let i = 0; i < tagNames.length; i++) {
|
||||||
const tagName = tagNames[i];
|
const tagName = tagNames[i];
|
||||||
|
@ -476,6 +536,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_getEmptyContent: function(section) {
|
_getEmptyContent: function(section) {
|
||||||
|
if (this.state.selectedTags.length > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
|
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
|
||||||
|
|
||||||
if (this.props.collapsed) {
|
if (this.props.collapsed) {
|
||||||
|
@ -486,28 +550,25 @@ module.exports = React.createClass({
|
||||||
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
|
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
|
||||||
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
|
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
|
||||||
|
|
||||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case 'im.vector.fake.direct':
|
case 'im.vector.fake.direct':
|
||||||
return <div className="mx_RoomList_emptySubListTip">
|
return <div className="mx_RoomList_emptySubListTip">
|
||||||
{ _tJsx(
|
{ _t(
|
||||||
"Press <StartChatButton> to start a chat with someone",
|
"Press <StartChatButton> to start a chat with someone",
|
||||||
[/<StartChatButton>/],
|
{},
|
||||||
[
|
{ 'StartChatButton': <StartChatButton size="16" callout={true} /> },
|
||||||
(sub) => <StartChatButton size="16" callout={true} />,
|
|
||||||
],
|
|
||||||
) }
|
) }
|
||||||
</div>;
|
</div>;
|
||||||
case 'im.vector.fake.recent':
|
case 'im.vector.fake.recent':
|
||||||
return <div className="mx_RoomList_emptySubListTip">
|
return <div className="mx_RoomList_emptySubListTip">
|
||||||
{ _tJsx(
|
{ _t(
|
||||||
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
|
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
|
||||||
" <RoomDirectoryButton> to browse the directory",
|
" <RoomDirectoryButton> to browse the directory",
|
||||||
[/<CreateRoomButton>/, /<RoomDirectoryButton>/],
|
{},
|
||||||
[
|
{
|
||||||
(sub) => <CreateRoomButton size="16" callout={true} />,
|
'CreateRoomButton': <CreateRoomButton size="16" callout={true} />,
|
||||||
(sub) => <RoomDirectoryButton size="16" callout={true} />,
|
'RoomDirectoryButton': <RoomDirectoryButton size="16" callout={true} />,
|
||||||
],
|
},
|
||||||
) }
|
) }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ const React = require('react');
|
||||||
const sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomPreviewBar',
|
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}) }
|
{ _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomPreviewBar_join_text">
|
<div className="mx_RoomPreviewBar_join_text">
|
||||||
{ _tJsx(
|
{ _t(
|
||||||
'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?',
|
'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>,
|
'acceptText': (sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>,
|
||||||
(sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>,
|
'declineText': (sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>,
|
||||||
],
|
},
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
{ emailMatchBlock }
|
{ emailMatchBlock }
|
||||||
|
@ -211,9 +211,9 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomPreviewBar_join_text">
|
<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.') }
|
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
|
||||||
<br />
|
<br />
|
||||||
{ _tJsx("<a>Click here</a> to join the discussion!",
|
{ _t("<a>Click here</a> to join the discussion!",
|
||||||
/<a>(.*?)<\/a>/,
|
{},
|
||||||
(sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a>,
|
{ 'a': (sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a> },
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,14 +17,14 @@ limitations under the License.
|
||||||
|
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t, _tJsx, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import ObjectUtils from '../../../ObjectUtils';
|
import ObjectUtils from '../../../ObjectUtils';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
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
|
// 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
|
// url preview settings
|
||||||
const ps = this.saveUrlPreviewSettings();
|
const ps = this.saveUrlPreviewSettings();
|
||||||
if (ps.length > 0) {
|
if (ps.length > 0) {
|
||||||
promises.push(ps);
|
ps.map((p) => promises.push(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
// related groups
|
// related groups
|
||||||
|
@ -363,26 +363,16 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
saveBlacklistUnverifiedDevicesPerRoom: function() {
|
saveBlacklistUnverifiedDevicesPerRoom: function() {
|
||||||
if (!this.refs.blacklistUnverified) return;
|
if (!this.refs.blacklistUnverifiedDevices) return;
|
||||||
if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) {
|
this.refs.blacklistUnverifiedDevices.save().then(() => {
|
||||||
this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked);
|
const value = SettingsStore.getValueAt(
|
||||||
}
|
SettingLevel.ROOM_DEVICE,
|
||||||
},
|
"blacklistUnverifiedDevices",
|
||||||
|
this.props.room.roomId,
|
||||||
_isRoomBlacklistUnverified: function() {
|
/*explicit=*/true,
|
||||||
const blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom;
|
);
|
||||||
if (blacklistUnverifiedDevicesPerRoom) {
|
|
||||||
return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId];
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
_setRoomBlacklistUnverified: function(value) {
|
|
||||||
const blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom || {};
|
|
||||||
blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value;
|
|
||||||
UserSettingsStore.setLocalSetting('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom);
|
|
||||||
|
|
||||||
this.props.room.setBlacklistUnverifiedDevices(value);
|
this.props.room.setBlacklistUnverifiedDevices(value);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_hasDiff: function(strA, strB) {
|
_hasDiff: function(strA, strB) {
|
||||||
|
@ -588,19 +578,20 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderEncryptionSection: function() {
|
_renderEncryptionSection: function() {
|
||||||
|
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const roomState = this.props.room.currentState;
|
const roomState = this.props.room.currentState;
|
||||||
const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
|
const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
|
||||||
const isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices;
|
|
||||||
const isRoomBlacklistUnverified = this._isRoomBlacklistUnverified();
|
|
||||||
|
|
||||||
const settings =
|
const settings = (
|
||||||
<label>
|
<SettingsFlag name="blacklistUnverifiedDevices"
|
||||||
<input type="checkbox" ref="blacklistUnverified"
|
level={SettingLevel.ROOM_DEVICE}
|
||||||
defaultChecked={isGlobalBlacklistUnverified || isRoomBlacklistUnverified}
|
roomId={this.props.room.roomId}
|
||||||
disabled={isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked)} />
|
manualSave={true}
|
||||||
{ _t('Never send encrypted messages to unverified devices in this room from this device') }.
|
ref="blacklistUnverifiedDevices"
|
||||||
</label>;
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
|
if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
|
||||||
return (
|
return (
|
||||||
|
@ -637,9 +628,7 @@ module.exports = React.createClass({
|
||||||
const ColorSettings = sdk.getComponent("room_settings.ColorSettings");
|
const ColorSettings = sdk.getComponent("room_settings.ColorSettings");
|
||||||
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||||
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
||||||
const EditableText = sdk.getComponent('elements.EditableText');
|
|
||||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const roomState = this.props.room.currentState;
|
const roomState = this.props.room.currentState;
|
||||||
|
@ -671,13 +660,11 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
let relatedGroupsSection;
|
const relatedGroupsSection = <RelatedGroupSettings ref="related_groups"
|
||||||
if (UserSettingsStore.isFeatureEnabled('feature_groups')) {
|
|
||||||
relatedGroupsSection = <RelatedGroupSettings ref="related_groups"
|
|
||||||
roomId={this.props.room.roomId}
|
roomId={this.props.room.roomId}
|
||||||
canSetRelatedGroups={roomState.mayClientSendStateEvent("m.room.related_groups", cli)}
|
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;
|
let userLevelsSection;
|
||||||
if (Object.keys(user_levels).length) {
|
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) {
|
if (canSetTag || self.state.tags) {
|
||||||
var tagsSection =
|
tagsSection =
|
||||||
<div className="mx_RoomSettings_tags">
|
<div className="mx_RoomSettings_tags">
|
||||||
{ _t("Tagged as: ") }{ canSetTag ?
|
{ _t("Tagged as: ") }{ canSetTag ?
|
||||||
(tags.map(function(tag, i) {
|
(tags.map(function(tag, i) {
|
||||||
|
@ -792,10 +779,10 @@ module.exports = React.createClass({
|
||||||
if (this.state.join_rule === "public" && aliasCount == 0) {
|
if (this.state.join_rule === "public" && aliasCount == 0) {
|
||||||
addressWarning =
|
addressWarning =
|
||||||
<div className="mx_RoomSettings_warning">
|
<div className="mx_RoomSettings_warning">
|
||||||
{ _tJsx(
|
{ _t(
|
||||||
'To link to a room it must have <a>an address</a>.',
|
'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>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -912,41 +899,41 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span>
|
<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>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages, you must be a') } </span>
|
<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>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room, you must be a') } </span>
|
<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>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room, you must be a') } </span>
|
<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>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users, you must be a') } </span>
|
<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>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users, you must be a') } </span>
|
<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>
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages, you must be a') } </span>
|
<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>
|
</div>
|
||||||
|
|
||||||
{ Object.keys(events_levels).map(function(event_type, i) {
|
{ Object.keys(events_levels).map(function(event_type, i) {
|
||||||
let label = plEventsToLabels[event_type];
|
let label = plEventsToLabels[event_type];
|
||||||
if (label) label = _t(label);
|
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 (
|
return (
|
||||||
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
||||||
<span className="mx_RoomSettings_powerLevelKey">{ label } </span>
|
<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]} />
|
controlled={false} disabled={!can_change_levels || current_user_level < events_levels[event_type]} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,7 +27,6 @@ const ContextualMenu = require('../../structures/ContextualMenu');
|
||||||
const RoomNotifs = require('../../../RoomNotifs');
|
const RoomNotifs = require('../../../RoomNotifs');
|
||||||
const FormattingUtils = require('../../../utils/FormattingUtils');
|
const FormattingUtils = require('../../../utils/FormattingUtils');
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
const UserSettingsStore = require('../../../UserSettingsStore');
|
|
||||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import classNames from 'classnames';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
export default class DevicesPanel extends React.Component {
|
export default class DevicesPanel extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
|
@ -29,11 +29,16 @@ export default class DevicesPanel extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
devices: undefined,
|
devices: undefined,
|
||||||
deviceLoadError: undefined,
|
deviceLoadError: undefined,
|
||||||
|
|
||||||
|
selectedDevices: [],
|
||||||
|
deleting: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
|
|
||||||
this._renderDevice = this._renderDevice.bind(this);
|
this._renderDevice = this._renderDevice.bind(this);
|
||||||
|
this._onDeviceSelectionToggled = this._onDeviceSelectionToggled.bind(this);
|
||||||
|
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -82,25 +87,78 @@ export default class DevicesPanel extends React.Component {
|
||||||
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
|
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDeviceDeleted(device) {
|
_onDeviceSelectionToggled(device) {
|
||||||
if (this._unmounted) { return; }
|
if (this._unmounted) { return; }
|
||||||
|
|
||||||
// delete the removed device from our list.
|
const deviceId = device.device_id;
|
||||||
const removed_id = device.device_id;
|
|
||||||
this.setState((state, props) => {
|
this.setState((state, props) => {
|
||||||
const newDevices = state.devices.filter(
|
// Make a copy of the selected devices, then add or remove the device
|
||||||
(d) => { return d.device_id != removed_id; },
|
const selectedDevices = state.selectedDevices.slice();
|
||||||
);
|
|
||||||
return { devices: newDevices };
|
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) {
|
_renderDevice(device) {
|
||||||
const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
|
const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
|
||||||
return (
|
return <DevicesPanelEntry
|
||||||
<DevicesPanelEntry key={device.device_id} device={device}
|
key={device.device_id}
|
||||||
onDeleted={()=>{this._onDeviceDeleted(device);}} />
|
device={device}
|
||||||
);
|
selected={this.state.selectedDevices.includes(device.device_id)}
|
||||||
|
onDeviceToggled={this._onDeviceSelectionToggled}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -124,6 +182,12 @@ export default class DevicesPanel extends React.Component {
|
||||||
|
|
||||||
devices.sort(this._deviceCompare);
|
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");
|
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<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_deviceId">{ _t("Device ID") }</div>
|
||||||
<div className="mx_DevicesPanel_deviceName">{ _t("Device Name") }</div>
|
<div className="mx_DevicesPanel_deviceName">{ _t("Device Name") }</div>
|
||||||
<div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</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>
|
</div>
|
||||||
{ devices.map(this._renderDevice) }
|
{ devices.map(this._renderDevice) }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,24 +19,15 @@ import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import Modal from '../../../Modal';
|
|
||||||
import DateUtils from '../../../DateUtils';
|
import DateUtils from '../../../DateUtils';
|
||||||
|
|
||||||
const AUTH_CACHE_AGE = 5 * 60 * 1000; // 5 minutes
|
|
||||||
|
|
||||||
export default class DevicesPanelEntry extends React.Component {
|
export default class DevicesPanelEntry extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
|
||||||
deleting: false,
|
|
||||||
deleteError: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
this.onDeviceToggled = this.onDeviceToggled.bind(this);
|
||||||
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
|
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
|
||||||
this._makeDeleteRequest = this._makeDeleteRequest.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -53,56 +44,8 @@ export default class DevicesPanelEntry extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDeleteClick() {
|
onDeviceToggled() {
|
||||||
this.setState({deleting: true});
|
this.props.onDeviceToggled(this.props.device);
|
||||||
|
|
||||||
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 });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -110,16 +53,6 @@ export default class DevicesPanelEntry extends React.Component {
|
||||||
|
|
||||||
const device = this.props.device;
|
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 = "";
|
let lastSeen = "";
|
||||||
if (device.last_seen_ts) {
|
if (device.last_seen_ts) {
|
||||||
const lastSeenDate = DateUtils.formatDate(new Date(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();
|
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 = '';
|
let myDeviceClass = '';
|
||||||
if (device.device_id === MatrixClientPeg.get().getDeviceId()) {
|
if (device.device_id === MatrixClientPeg.get().getDeviceId()) {
|
||||||
myDeviceClass = " mx_DevicesPanel_myDevice";
|
myDeviceClass = " mx_DevicesPanel_myDevice";
|
||||||
|
@ -159,7 +80,7 @@ export default class DevicesPanelEntry extends React.Component {
|
||||||
{ lastSeen }
|
{ lastSeen }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_DevicesPanel_deviceButtons">
|
<div className="mx_DevicesPanel_deviceButtons">
|
||||||
{ deleteButton }
|
<input type="checkbox" onClick={this.onDeviceToggled} checked={this.props.selected} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -168,13 +89,9 @@ export default class DevicesPanelEntry extends React.Component {
|
||||||
|
|
||||||
DevicesPanelEntry.propTypes = {
|
DevicesPanelEntry.propTypes = {
|
||||||
device: React.PropTypes.object.isRequired,
|
device: React.PropTypes.object.isRequired,
|
||||||
onDeleted: React.PropTypes.func,
|
onDeviceToggled: React.PropTypes.func,
|
||||||
};
|
|
||||||
|
|
||||||
DevicesPanelEntry.contextTypes = {
|
|
||||||
authCache: React.PropTypes.object,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
DevicesPanelEntry.defaultProps = {
|
DevicesPanelEntry.defaultProps = {
|
||||||
onDeleted: function() {},
|
onDeviceToggled: function() {},
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,7 +23,7 @@ import classNames from 'classnames';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'VideoView',
|
displayName: 'VideoView',
|
||||||
|
@ -113,7 +113,7 @@ module.exports = React.createClass({
|
||||||
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
|
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
|
||||||
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
|
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
|
||||||
{ "mx_VideoView_localVideoFeed_flipped":
|
{ "mx_VideoView_localVideoFeed_flipped":
|
||||||
UserSettingsStore.getSyncedSetting('VideoView.flipVideoHorizontally', false),
|
SettingsStore.getValue('VideoView.flipVideoHorizontally'),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -44,15 +44,15 @@
|
||||||
"Create new room": "Založit novou místnost",
|
"Create new room": "Založit novou místnost",
|
||||||
"Room directory": "Adresář místností",
|
"Room directory": "Adresář místností",
|
||||||
"Start chat": "Začít chat",
|
"Start chat": "Začít chat",
|
||||||
"Options": "Možnosti",
|
"Options": "Volby",
|
||||||
"Register": "Zaregistrovat",
|
"Register": "Zaregistrovat",
|
||||||
"Cancel": "Storno",
|
"Cancel": "Storno",
|
||||||
"Error": "Chyba",
|
"Error": "Chyba",
|
||||||
"Favourite": "V oblíbených",
|
"Favourite": "V oblíbených",
|
||||||
"Mute": "Ztlumit",
|
"Mute": "Ztlumit",
|
||||||
"Continue": "Pokračovat",
|
"Continue": "Pokračovat",
|
||||||
"Failed to change password. Is your password correct?": "Nepodařilo se změnit heslo. Je vaše heslo správné?",
|
"Failed to change password. Is your password correct?": "Nepodařilo se změnit heslo. Zadáváte své heslo správně?",
|
||||||
"Operation failed": "Chyba operace",
|
"Operation failed": "Operace se nezdařila",
|
||||||
"Remove": "Odebrat",
|
"Remove": "Odebrat",
|
||||||
"unknown error code": "neznámý kód chyby",
|
"unknown error code": "neznámý kód chyby",
|
||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
|
@ -140,7 +140,7 @@
|
||||||
"Decline": "Odmítnout",
|
"Decline": "Odmítnout",
|
||||||
"Decrypt %(text)s": "Dešifrovat %(text)s",
|
"Decrypt %(text)s": "Dešifrovat %(text)s",
|
||||||
"Decryption error": "Chyba dešifrování",
|
"Decryption error": "Chyba dešifrování",
|
||||||
"Delete": "Vymazat",
|
"Delete": "Smazat",
|
||||||
"Delete widget": "Vymazat widget",
|
"Delete widget": "Vymazat widget",
|
||||||
"Default": "Výchozí",
|
"Default": "Výchozí",
|
||||||
"Device already verified!": "Zařízení již bylo ověřeno!",
|
"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 delete device": "Nepodařilo se vymazat zařízení",
|
||||||
"Failed to join room": "Vstup do místnosti se nezdařil",
|
"Failed to join room": "Vstup do místnosti se nezdařil",
|
||||||
"Failed to kick": "Vykopnutí se nezdařilo",
|
"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 mute user": "Ztlumení uživatele se nezdařilo",
|
||||||
"Failed to send email": "Odeslání e-mailu 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 save settings": "Nepodařilo se uložit nastavení",
|
||||||
"Failed to reject invitation": "Odmítnutí pozvánky se nezdařilo",
|
"Failed to reject invitation": "Nepodařilo se odmítnout pozvání",
|
||||||
"Failed to reject invite": "Odmítnutí pozvání se nezdařilo",
|
"Failed to reject invite": "Nepodařilo se odmítnout pozvánku",
|
||||||
"Failed to send request.": "Odeslání žádosti se nezdařilo.",
|
"Failed to send request.": "Odeslání žádosti se nezdařilo.",
|
||||||
"Failed to set avatar.": "Nastavení avataru 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 display name": "Nepodařilo se nastavit zobrazované jméno",
|
||||||
"Failed to set up conference call": "Nastavení konferenčního hovoru se nezdařilo",
|
"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 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",
|
"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",
|
"Failure to create room": "Vytvoření místnosti se nezdařilo",
|
||||||
"Forget room": "Zapomenout místnost",
|
"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.",
|
"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 one other are typing": "%(names)s a jeden další píší",
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s a %(lastPerson)s píší",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s a %(lastPerson)s píší",
|
||||||
|
@ -307,10 +307,10 @@
|
||||||
"Send anyway": "Přesto poslat",
|
"Send anyway": "Přesto poslat",
|
||||||
"Sender device information": "Informace o odesilatelově zařízení",
|
"Sender device information": "Informace o odesilatelově zařízení",
|
||||||
"Send Reset Email": "Poslat resetovací e-mail",
|
"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.",
|
"%(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.",
|
"%(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 error": "Chyba serveru",
|
||||||
"Server may be unavailable or overloaded": "Server může být nedostupný nebo přetížený",
|
"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 :(",
|
"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",
|
||||||
"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",
|
"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)",
|
"Active call (%(roomName)s)": "Probíhající hovor (%(roomName)s)",
|
||||||
"An email has been sent to": "Email byl odeslán odeslán na",
|
"An email has been sent to": "E-mail byl odeslán odeslán na",
|
||||||
"%(senderName)s banned %(targetName)s.": "%(senderName)s zablokoval/a %(targetName)s.",
|
"%(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 homeserveru přes HTTP pokud je v adresním řádku HTTPS. Buď použijte HTTPS nebo <a>povolte nebezpečné scripty</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>.": "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 zobrazované jméno z %(oldDisplayName)s na %(displayName)s.",
|
"%(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 here to fix": "Klikněte zde pro opravu",
|
||||||
"Click to mute video": "Klikněte pro zakázání videa",
|
"Click to mute video": "Klikněte pro zakázání videa",
|
||||||
"click to reveal": "klikněte pro odhalení",
|
"click to reveal": "klikněte pro odhalení",
|
||||||
"Click to unmute video": "Klikněte pro povolení videa",
|
"Click to unmute video": "Klikněte pro povolení videa",
|
||||||
"Click to unmute audio": "Klikněte pro povolení zvuku",
|
"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",
|
"Displays action": "Zobrazí akci",
|
||||||
"Do you want to load widget from URL:": "Chcete načíst widget z URL:",
|
"Do you want to load widget from URL:": "Chcete načíst widget z URL:",
|
||||||
"Ed25519 fingerprint": "Ed25519 otisk",
|
"Ed25519 fingerprint": "Ed25519 otisk",
|
||||||
"Fill screen": "Vyplnit obrazovku",
|
"Fill screen": "Vyplnit obrazovku",
|
||||||
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s z %(fromPowerLevel)s na %(toPowerLevel)s",
|
"%(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 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 phone number is already in use": "Toto číslo se již používá",
|
||||||
"This room is not accessible by remote Matrix servers": "Tahle místnost není přístupná vzdálenými Matrix servery",
|
"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 místnosti je",
|
"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 emailovou adresu spojenou s vaším účtem",
|
"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": "obnovit",
|
"to restore": "obnovíte",
|
||||||
"to tag direct chat": "oštítkovat přímý chat",
|
"to tag direct chat": "oštítkujete 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.",
|
"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 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 off": "Vypnout Markdown",
|
||||||
"Turn Markdown on": "Zapnout 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).",
|
"%(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 emailovou adresu",
|
"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 create widget.": "Nepodařilo se vytvořit widget.",
|
||||||
"Unable to remove contact information": "Nepodařilo se smazat kontaktní údaje",
|
"Unable to remove contact information": "Nepodařilo se smazat kontaktní údaje",
|
||||||
"Unable to verify email address.": "Nepodařilo se ověřit emailovou adresu.",
|
"Unable to verify email address.": "Nepodařilo se ověřit e-mailovou adresu.",
|
||||||
"Unban": "Odblokovat",
|
"Unban": "Přijmout zpět",
|
||||||
"Unbans user with given id": "Odblokuje uživatele s daným id",
|
"Unbans user with given id": "Přijme zpět uživatele s daným id",
|
||||||
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s odblokoval/a %(targetName)s.",
|
"%(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řiřazenou Vašemu účtu.",
|
"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 capture screen": "Nepodařilo se zachytit obrazovku",
|
||||||
"Unable to enable Notifications": "Nepodařilo se povolit Notifikace",
|
"Unable to enable Notifications": "Nepodařilo se povolit upozornění",
|
||||||
"Unable to load device list": "Nepodařilo se načíst list zařízení",
|
"Unable to load device list": "Nepodařilo se načíst seznam zařízení",
|
||||||
"Undecryptable": "Nerozšifrovatelné",
|
"Undecryptable": "Nerozšifrovatelné",
|
||||||
"unencrypted": "nešifrované",
|
"unencrypted": "nešifrované",
|
||||||
"Unencrypted message": "Nešifrovaná zpráva",
|
"Unencrypted message": "Nešifrovaná zpráva",
|
||||||
|
@ -421,15 +421,15 @@
|
||||||
"Unknown room %(roomId)s": "Neznámá místnost %(roomId)s",
|
"Unknown room %(roomId)s": "Neznámá místnost %(roomId)s",
|
||||||
"Unknown (user, device) pair:": "Neznámý pár (uživatel, zařízení):",
|
"Unknown (user, device) pair:": "Neznámý pár (uživatel, zařízení):",
|
||||||
"Unmute": "Povolit",
|
"Unmute": "Povolit",
|
||||||
"Unnamed Room": "Nepojmenovaná Místnost",
|
"Unnamed Room": "Nepojmenovaná místnost",
|
||||||
"Unrecognised command:": "Nerozpoznaný příkaz:",
|
"Unrecognised command:": "Nerozpoznaný příkaz:",
|
||||||
"Unrecognised room alias:": "Nerozpoznaný alias místnosti:",
|
"Unrecognised room alias:": "Nerozpoznaný alias místnosti:",
|
||||||
"Unverified": "Neověřený",
|
"Unverified": "Neověřený",
|
||||||
"Uploading %(filename)s and %(count)s others|zero": "Nahrávám %(filename)s",
|
"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|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ší",
|
"Uploading %(filename)s and %(count)s others|other": "Nahrávám %(filename)s a %(count)s další",
|
||||||
"Upload Failed": "Nahrávání Selhalo",
|
"Upload Failed": "Nahrávání selhalo",
|
||||||
"Upload Files": "Nahrát Soubory",
|
"Upload Files": "Nahrát soubory",
|
||||||
"Upload file": "Nahrát soubor",
|
"Upload file": "Nahrát soubor",
|
||||||
"Upload new:": "Nahrát nový:",
|
"Upload new:": "Nahrát nový:",
|
||||||
"Usage": "Použití",
|
"Usage": "Použití",
|
||||||
|
@ -439,16 +439,238 @@
|
||||||
"User Interface": "Uživatelské rozhraní",
|
"User Interface": "Uživatelské rozhraní",
|
||||||
"%(user)s is a": "%(user)s je",
|
"%(user)s is a": "%(user)s je",
|
||||||
"User name": "Uživatelské jméno",
|
"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é",
|
"Users": "Uživatelé",
|
||||||
"User": "Uživatel",
|
"User": "Uživatel",
|
||||||
"Verification Pending": "Čeká na ověření",
|
"Verification Pending": "Čeká na ověření",
|
||||||
"Verification": "Ověření",
|
"Verification": "Ověření",
|
||||||
"verified": "ověreno",
|
"verified": "ověreno",
|
||||||
"Verified": "Ověřeno",
|
"Verified": "Ověřeno",
|
||||||
"Verified key": "Ověřen klíč",
|
"Verified key": "Ověřený klíč",
|
||||||
"(no answer)": "(žádná odpověď)",
|
"(no answer)": "(žádná odpověď)",
|
||||||
"(unknown failure: %(reason)s)": "(neznámá chyba: %(reason)s)",
|
"(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: cannot be disabled again!)": "(varování: nepůjde znovu zakázat!)",
|
||||||
"WARNING: Device already verified, but keys do NOT MATCH!": "VAROVÁNÍ: Zařízení ověřeno, ale klíče se NESHODUJÍ!"
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,10 @@
|
||||||
"User ID": "Benutzer-ID",
|
"User ID": "Benutzer-ID",
|
||||||
"Curve25519 identity key": "Curve25519-Identitäts-Schlüssel",
|
"Curve25519 identity key": "Curve25519-Identitäts-Schlüssel",
|
||||||
"Claimed Ed25519 fingerprint key": "Geforderter Ed25519-Fingerprint-Schlüssel",
|
"Claimed Ed25519 fingerprint key": "Geforderter Ed25519-Fingerprint-Schlüssel",
|
||||||
"none": "keiner",
|
"none": "nicht vorhanden",
|
||||||
"Algorithm": "Algorithmus",
|
"Algorithm": "Algorithmus",
|
||||||
"unencrypted": "unverschlüsselt",
|
"unencrypted": "unverschlüsselt",
|
||||||
"Decryption error": "Entschlüsselungs Fehler",
|
"Decryption error": "Fehler beim Entschlüsseln",
|
||||||
"Session ID": "Sitzungs-ID",
|
"Session ID": "Sitzungs-ID",
|
||||||
"End-to-end encryption information": "Informationen zur Ende-zu-Ende-Verschlüsselung",
|
"End-to-end encryption information": "Informationen zur Ende-zu-Ende-Verschlüsselung",
|
||||||
"Event information": "Ereignis-Information",
|
"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",
|
"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",
|
"Failed to send email": "Fehler beim Senden der E-Mail",
|
||||||
"Account": "Benutzerkonto",
|
"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",
|
"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",
|
"Can't load user settings": "Benutzereinstellungen können nicht geladen werden",
|
||||||
"Clear Cache": "Cache leeren",
|
"Clear Cache": "Cache leeren",
|
||||||
|
@ -138,7 +138,7 @@
|
||||||
"Scroll to unread messages": "Zu den ungelesenen Nachrichten scrollen",
|
"Scroll to unread messages": "Zu den ungelesenen Nachrichten scrollen",
|
||||||
"Send Invites": "Einladungen senden",
|
"Send Invites": "Einladungen senden",
|
||||||
"Send Reset Email": "E-Mail zum Zurücksetzen 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",
|
"sent a video": "hat ein Video gesendet",
|
||||||
"Server may be unavailable or overloaded": "Server ist eventuell nicht verfügbar oder überlastet",
|
"Server may be unavailable or overloaded": "Server ist eventuell nicht verfügbar oder überlastet",
|
||||||
"Settings": "Einstellungen",
|
"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 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",
|
"This room's internal ID is": "Die interne ID dieses Raumes ist",
|
||||||
"Admin": "Administrator",
|
"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",
|
"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",
|
"Disable inline URL previews by default": "URL-Vorschau im Chat standardmäßig deaktivieren",
|
||||||
"Labs": "Labor",
|
"Labs": "Labor",
|
||||||
|
@ -262,7 +262,7 @@
|
||||||
"Encrypt room": "Raum verschlüsseln",
|
"Encrypt room": "Raum verschlüsseln",
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s und %(lastPerson)s schreiben",
|
"%(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 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",
|
"%(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 answered the call.": "%(senderName)s hat den Anruf angenommen.",
|
||||||
"%(senderName)s banned %(targetName)s.": "%(senderName)s hat %(targetName)s dauerhaft aus dem Raum verbannt.",
|
"%(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 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, 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 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).",
|
"%(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 room_id in request": "Fehlende room_id in Anfrage",
|
||||||
"Missing user_id in request": "Fehlende user_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 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.",
|
"%(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",
|
"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 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 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.",
|
"%(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",
|
"bullet": "Aufzählung",
|
||||||
"Click to unmute video": "Klicken, um die Video-Stummschaltung zu deaktivieren",
|
"Click to unmute video": "Klicken, um die Video-Stummschaltung zu deaktivieren",
|
||||||
"Click to unmute audio": "Klicken, um den Ton wieder einzuschalten",
|
"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",
|
"Failed to toggle moderator status": "Umschalten des Moderator-Status fehlgeschlagen",
|
||||||
"Enable encryption": "Verschlüsselung aktivieren",
|
"Enable encryption": "Verschlüsselung aktivieren",
|
||||||
"The main address for this room is": "Die Hauptadresse für diesen Raum ist",
|
"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",
|
"was kicked %(repeats)s times": "wurde %(repeats)s-mal gekickt",
|
||||||
"were kicked": "wurden gekickt",
|
"were kicked": "wurden gekickt",
|
||||||
"%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)shaben ihren Namen %(repeats)s mal geändert",
|
"%(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",
|
"%(severalUsers)schanged their name": "%(severalUsers)shaben ihre Namen geändert",
|
||||||
"%(oneUser)schanged their name": "%(oneUser)shat den 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",
|
"%(severalUsers)schanged their avatar %(repeats)s times": "%(severalUsers)shaben %(repeats)s mal ihr Profilbild geändert",
|
||||||
|
@ -596,7 +596,7 @@
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
"Device already verified!": "Gerät bereits verifiziert!",
|
"Device already verified!": "Gerät bereits verifiziert!",
|
||||||
"Export": "Export",
|
"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",
|
"Import": "Importieren",
|
||||||
"Incorrect username and/or password.": "Inkorrekter Nutzername und/oder Passwort.",
|
"Incorrect username and/or password.": "Inkorrekter Nutzername und/oder Passwort.",
|
||||||
"Results from DuckDuckGo": "Ergebnisse von DuckDuckGo",
|
"Results from DuckDuckGo": "Ergebnisse von DuckDuckGo",
|
||||||
|
@ -696,7 +696,7 @@
|
||||||
"This room": "In diesem Raum",
|
"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.",
|
"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",
|
"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",
|
"Unencrypted message": "Nicht verschlüsselbare Nachricht",
|
||||||
"unknown caller": "Unbekannter Anrufer",
|
"unknown caller": "Unbekannter Anrufer",
|
||||||
"Unnamed Room": "Unbenannter Raum",
|
"Unnamed Room": "Unbenannter Raum",
|
||||||
|
@ -739,7 +739,7 @@
|
||||||
"Changes colour scheme of current room": "Ändere Farbschema des aktuellen Raumes",
|
"Changes colour scheme of current room": "Ändere Farbschema des aktuellen Raumes",
|
||||||
"Delete widget": "Widget entfernen",
|
"Delete widget": "Widget entfernen",
|
||||||
"Define the power level of a user": "Setze das Berechtigungslevel eines Benutzers",
|
"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",
|
"Enable automatic language detection for syntax highlighting": "Automatische Spracherkennung für die Syntax-Hervorhebung aktivieren",
|
||||||
"Hide Apps": "Apps verbergen",
|
"Hide Apps": "Apps verbergen",
|
||||||
"Hide join/leave messages (invites/kicks/bans unaffected)": "Betreten-/Verlassen-Benachrichtigungen verbergen (gilt nicht für Einladungen/Kicks/Bans)",
|
"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:",
|
"Do you want to load widget from URL:": "Möchtest du das Widget von folgender URL laden:",
|
||||||
"Integrations Error": "Integrations-Error",
|
"Integrations Error": "Integrations-Error",
|
||||||
"NOTE: Apps are not end-to-end encrypted": "BEACHTE: Apps sind nicht Ende-zu-Ende verschlüsselt",
|
"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 added by %(senderName)s": "%(senderName)s hat das Widget %(widgetName)s hinzugefügt",
|
||||||
"%(widgetName)s widget removed by %(senderName)s": "Widget \"%(widgetName)s\" von %(senderName)s entfernt",
|
"%(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",
|
"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",
|
"%(widgetName)s widget modified by %(senderName)s": "Das Widget '%(widgetName)s' wurde von %(senderName)s bearbeitet",
|
||||||
"Copied!": "Kopiert!",
|
"Copied!": "Kopiert!",
|
||||||
"Failed to copy": "Kopieren fehlgeschlagen",
|
"Failed to copy": "Kopieren fehlgeschlagen",
|
||||||
"Ignored Users": "Ignorierte Benutzer",
|
"Ignored Users": "Ignorierte Benutzer",
|
||||||
"Ignore": "Ignorieren",
|
"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",
|
"You are no longer ignoring %(userId)s": "%(userId)s wird nicht mehr ignoriert",
|
||||||
"Message removed by %(userId)s": "Nachricht wurde von %(userId)s entfernt",
|
"Message removed by %(userId)s": "Nachricht wurde von %(userId)s entfernt",
|
||||||
"Name or matrix ID": "Name oder Matrix-ID",
|
"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.",
|
"You have entered an invalid address.": "Du hast eine ungültige Adresse eingegeben.",
|
||||||
"Matrix ID": "Matrix-ID",
|
"Matrix ID": "Matrix-ID",
|
||||||
"Advanced options": "Erweiterte Optionen",
|
"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!",
|
"This setting cannot be changed later!": "Diese Einstellung kann nachträglich nicht mehr geändert werden!",
|
||||||
"Unignore": "Entignorieren",
|
"Unignore": "Entignorieren",
|
||||||
"User Options": "Benutzer-Optionen",
|
"User Options": "Benutzer-Optionen",
|
||||||
"Unignored user": "Benutzer entignoriert",
|
"Unignored user": "Benutzer nicht mehr ignoriert",
|
||||||
"Ignored user": "Benutzer ignoriert",
|
"Ignored user": "Benutzer ignoriert",
|
||||||
"Stops ignoring a user, showing their messages going forward": "Beendet das Ignorieren eines Benutzers, nachfolgende Nachrichten werden wieder angezeigt",
|
"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",
|
"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",
|
"Disable Emoji suggestions while typing": "Emoji-Vorschläge während des Schreibens deaktivieren",
|
||||||
"Banned by %(displayName)s": "Gebannt von %(displayName)s",
|
"Banned by %(displayName)s": "Verbannt von %(displayName)s",
|
||||||
"To send messages, you must be a": "Um Nachrichten zu senden musst du sein ein",
|
"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 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 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 ban users, you must be a": "Notwendiges Berechtigungslevel, um Benutzer zu verbannen:",
|
||||||
"To remove other users' messages, you must be a": "Um Nachrichten von Benutzern zu löschen, musst du sein ein",
|
"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": "Um Ereignisse desTyps <eventType/> zu senden, musst du sein ein",
|
"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": "Um das Raumbild zu ändern, musst du sein ein",
|
"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": "Um den Raumnamen zu ändern, musst du sein ein",
|
"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": "Um die Hauptadresse des Raumes zu ändern, musst du sein ein",
|
"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": "Um die Sichtbarkeit des bisherigen Chatverlaufs zu ändern, musst du sein ein",
|
"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": "Um Berechtigungen in diesem Raum zu ändern, musst du sein ein",
|
"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": "Um das Thema zu ändern, musst du sein ein",
|
"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": "Um Widgets in dem Raum zu ändern, musst du sein ein",
|
"To modify widgets in the room, you must be a": "Notwendiges Berechtigungslevel, um Widgets in diesem Raum zu ändern",
|
||||||
"Description": "Beschreibung",
|
"Description": "Beschreibung",
|
||||||
"Unable to accept invite": "Einladung kann nicht akzeptiert werden",
|
"Unable to accept invite": "Einladung kann nicht akzeptiert werden",
|
||||||
"Failed to invite users to %(groupId)s": "Benutzer konnten nicht in %(groupId)s eingeladen 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:",
|
"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?",
|
"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",
|
"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 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": "Raum konnte nicht aus der Übersicht von %(groupId)s entfernt 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.",
|
"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",
|
"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.",
|
"The user '%(displayName)s' could not be removed from the summary.": "Der Benutzer '%(displayName)s' konnte nicht aus der Übersicht entfernt werden.",
|
||||||
"Unknown": "Unbekannt",
|
"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",
|
"Matrix Room ID": "Matrix-Raum-ID",
|
||||||
"email address": "E-Mail-Adresse",
|
"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",
|
"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?",
|
"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",
|
"Invites sent": "Einladungen gesendet",
|
||||||
|
@ -845,17 +845,161 @@
|
||||||
"Pinned Messages": "Angeheftete Nachrichten",
|
"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.",
|
"%(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",
|
"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",
|
"Publish this community on your profile": "Diese Community in deinem Profil veröffentlichen",
|
||||||
"Long Description (HTML)": "Lange Beschreibung (HTML)",
|
"Long Description (HTML)": "Lange Beschreibung (HTML)",
|
||||||
"Jump to message": "Zur Nachricht springen",
|
"Jump to message": "Zur Nachricht springen",
|
||||||
"No pinned messages.": "Keine angehefteten Nachrichten.",
|
"No pinned messages.": "Keine angehefteten Nachrichten vorhanden.",
|
||||||
"Loading...": "Lade...",
|
"Loading...": "Lade...",
|
||||||
"Unpin Message": "Nachricht losheften",
|
"Unpin Message": "Nachricht nicht mehr anheften",
|
||||||
"Unnamed room": "Unbenannter Raum",
|
"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",
|
"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 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!"
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
"Name or matrix ID": "Name or matrix ID",
|
"Name or matrix ID": "Name or matrix ID",
|
||||||
"Invite to Community": "Invite to Community",
|
"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?",
|
"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",
|
"Add rooms to the community": "Add rooms to the community",
|
||||||
"Room name or alias": "Room name or alias",
|
"Room name or alias": "Room name or alias",
|
||||||
"Add to community": "Add to community",
|
"Add to community": "Add to community",
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
"This email address was not found": "This email address was not found",
|
"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.",
|
"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",
|
"Default": "Default",
|
||||||
"User": "User",
|
"Restricted": "Restricted",
|
||||||
"Moderator": "Moderator",
|
"Moderator": "Moderator",
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
"Start a chat": "Start a chat",
|
"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 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 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",
|
"%(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",
|
"%(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|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",
|
"%(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",
|
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
|
||||||
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
|
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
|
||||||
"Failed to join room": "Failed to join room",
|
"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)",
|
"Active call (%(roomName)s)": "Active call (%(roomName)s)",
|
||||||
"unknown caller": "unknown caller",
|
"unknown caller": "unknown caller",
|
||||||
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
|
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
|
||||||
|
@ -194,13 +219,14 @@
|
||||||
"Change Password": "Change Password",
|
"Change Password": "Change Password",
|
||||||
"Your home server does not support device management.": "Your home server does not support device management.",
|
"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",
|
"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 ID": "Device ID",
|
||||||
"Device Name": "Device Name",
|
"Device Name": "Device Name",
|
||||||
"Last seen": "Last seen",
|
"Last seen": "Last seen",
|
||||||
|
"Select devices": "Select devices",
|
||||||
"Failed to set display name": "Failed to set display name",
|
"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",
|
"Disable Notifications": "Disable Notifications",
|
||||||
"Enable Notifications": "Enable Notifications",
|
"Enable Notifications": "Enable Notifications",
|
||||||
"Cannot add any more widgets": "Cannot add any more widgets",
|
"Cannot add any more widgets": "Cannot add any more widgets",
|
||||||
|
@ -295,10 +321,14 @@
|
||||||
"No pinned messages.": "No pinned messages.",
|
"No pinned messages.": "No pinned messages.",
|
||||||
"Loading...": "Loading...",
|
"Loading...": "Loading...",
|
||||||
"Pinned Messages": "Pinned Messages",
|
"Pinned Messages": "Pinned Messages",
|
||||||
"for %(amount)ss": "for %(amount)ss",
|
"%(duration)ss": "%(duration)ss",
|
||||||
"for %(amount)sm": "for %(amount)sm",
|
"%(duration)sm": "%(duration)sm",
|
||||||
"for %(amount)sh": "for %(amount)sh",
|
"%(duration)sh": "%(duration)sh",
|
||||||
"for %(amount)sd": "for %(amount)sd",
|
"%(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",
|
"Online": "Online",
|
||||||
"Idle": "Idle",
|
"Idle": "Idle",
|
||||||
"Offline": "Offline",
|
"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",
|
"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)",
|
"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",
|
"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",
|
"Enable encryption": "Enable encryption",
|
||||||
"(warning: cannot be disabled again!)": "(warning: cannot be disabled again!)",
|
"(warning: cannot be disabled again!)": "(warning: cannot be disabled again!)",
|
||||||
"Encryption is enabled in this room": "Encryption is enabled in this room",
|
"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 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 were invited)": "Members only (since they were invited)",
|
||||||
"Members only (since they joined)": "Members only (since they joined)",
|
"Members only (since they joined)": "Members only (since they joined)",
|
||||||
"Room Colour": "Room Colour",
|
|
||||||
"Permissions": "Permissions",
|
"Permissions": "Permissions",
|
||||||
"The default role for new room members is": "The default role for new room members is",
|
"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",
|
"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)",
|
"New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)",
|
||||||
"Invalid community ID": "Invalid community ID",
|
"Invalid community ID": "Invalid community ID",
|
||||||
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID",
|
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID",
|
||||||
"Related Communities": "Related Communities",
|
"Flair": "Flair",
|
||||||
"Related communities for this room:": "Related communities for this room:",
|
"Showing flair for these communities:": "Showing flair for these communities:",
|
||||||
"This room has no related communities": "This room has no related 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)",
|
"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>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",
|
"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 audio": "Error decrypting audio",
|
||||||
"Error decrypting attachment": "Error decrypting attachment",
|
"Error decrypting attachment": "Error decrypting attachment",
|
||||||
"Decrypt %(text)s": "Decrypt %(text)s",
|
"Decrypt %(text)s": "Decrypt %(text)s",
|
||||||
|
@ -476,6 +500,7 @@
|
||||||
"Please enter the code it contains:": "Please enter the code it contains:",
|
"Please enter the code it contains:": "Please enter the code it contains:",
|
||||||
"Start authentication": "Start authentication",
|
"Start authentication": "Start authentication",
|
||||||
"powered by Matrix": "powered by Matrix",
|
"powered by Matrix": "powered by Matrix",
|
||||||
|
"Username on %(hs)s": "Username on %(hs)s",
|
||||||
"User name": "User name",
|
"User name": "User name",
|
||||||
"Mobile phone number": "Mobile phone number",
|
"Mobile phone number": "Mobile phone number",
|
||||||
"Forgot your password?": "Forgot your password?",
|
"Forgot your password?": "Forgot your password?",
|
||||||
|
@ -499,6 +524,8 @@
|
||||||
"Failed to withdraw invitation": "Failed to withdraw invitation",
|
"Failed to withdraw invitation": "Failed to withdraw invitation",
|
||||||
"Failed to remove user from community": "Failed to remove user from community",
|
"Failed to remove user from community": "Failed to remove user from community",
|
||||||
"Filter community members": "Filter community members",
|
"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?",
|
"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.",
|
"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",
|
"Remove": "Remove",
|
||||||
|
@ -510,6 +537,9 @@
|
||||||
"Visible to everyone": "Visible to everyone",
|
"Visible to everyone": "Visible to everyone",
|
||||||
"Only visible to community members": "Only visible to community members",
|
"Only visible to community members": "Only visible to community members",
|
||||||
"Filter community rooms": "Filter community rooms",
|
"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",
|
"Unknown Address": "Unknown Address",
|
||||||
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
|
"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:",
|
"Do you want to load widget from URL:": "Do you want to load widget from URL:",
|
||||||
|
@ -525,6 +555,8 @@
|
||||||
"Unverify": "Unverify",
|
"Unverify": "Unverify",
|
||||||
"Verify...": "Verify...",
|
"Verify...": "Verify...",
|
||||||
"No results": "No results",
|
"No results": "No results",
|
||||||
|
"Delete": "Delete",
|
||||||
|
"Communities": "Communities",
|
||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
"Integrations Error": "Integrations Error",
|
"Integrations Error": "Integrations Error",
|
||||||
"Could not connect to the integration server": "Could not connect to the integration server",
|
"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",
|
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
|
||||||
"collapse": "collapse",
|
"collapse": "collapse",
|
||||||
"expand": "expand",
|
"expand": "expand",
|
||||||
|
"Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
|
||||||
"Custom level": "Custom level",
|
"Custom level": "Custom level",
|
||||||
"Room directory": "Room directory",
|
"Room directory": "Room directory",
|
||||||
"Start chat": "Start chat",
|
"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",
|
"%(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 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",
|
"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!",
|
"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)",
|
"Long Description (HTML)": "Long Description (HTML)",
|
||||||
"Description": "Description",
|
"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.",
|
"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",
|
"Logout": "Logout",
|
||||||
"Your Communities": "Your Communities",
|
"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",
|
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
|
||||||
"Create a new community": "Create a new community",
|
"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.",
|
"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",
|
"Scroll to bottom of page": "Scroll to bottom of page",
|
||||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
"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.",
|
"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|other": "%(count)s new messages",
|
||||||
"%(count)s new messages|one": "%(count)s new message",
|
"%(count)s new messages|one": "%(count)s new message",
|
||||||
"Active call": "Active call",
|
"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 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?",
|
"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.",
|
"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|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|zero": "Uploading %(filename)s",
|
||||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
"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",
|
"Light theme": "Light theme",
|
||||||
"Dark theme": "Dark theme",
|
"Dark theme": "Dark theme",
|
||||||
|
"Status.im theme": "Status.im theme",
|
||||||
"Can't load user settings": "Can't load user settings",
|
"Can't load user settings": "Can't load user settings",
|
||||||
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
|
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
|
||||||
"Sign out": "Sign out",
|
"Sign out": "Sign out",
|
||||||
|
@ -795,7 +808,6 @@
|
||||||
"Interface Language": "Interface Language",
|
"Interface Language": "Interface Language",
|
||||||
"User Interface": "User Interface",
|
"User Interface": "User Interface",
|
||||||
"Autocomplete Delay (ms):": "Autocomplete Delay (ms):",
|
"Autocomplete Delay (ms):": "Autocomplete Delay (ms):",
|
||||||
"Disable inline URL previews by default": "Disable inline URL previews by default",
|
|
||||||
"<not supported>": "<not supported>",
|
"<not supported>": "<not supported>",
|
||||||
"Import E2E room keys": "Import E2E room keys",
|
"Import E2E room keys": "Import E2E room keys",
|
||||||
"Cryptography": "Cryptography",
|
"Cryptography": "Cryptography",
|
||||||
|
@ -860,14 +872,15 @@
|
||||||
"Create an account": "Create an account",
|
"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.",
|
"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.",
|
"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.",
|
"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",
|
"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.",
|
"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 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.",
|
"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",
|
"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",
|
"Failed to fetch avatar URL": "Failed to fetch avatar URL",
|
||||||
"Set a display name:": "Set a display name:",
|
"Set a display name:": "Set a display name:",
|
||||||
"Upload an avatar:": "Upload an avatar:",
|
"Upload an avatar:": "Upload an avatar:",
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
|
@ -370,7 +370,7 @@
|
||||||
"Warning!": "Attention !",
|
"Warning!": "Attention !",
|
||||||
"Who can access this room?": "Qui peut accéder au salon ?",
|
"Who can access this room?": "Qui peut accéder au salon ?",
|
||||||
"Who can read history?": "Qui peut lire l'historique ?",
|
"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 ?",
|
"Who would you like to communicate with?": "Avec qui voulez-vous communiquer ?",
|
||||||
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s a annulé l’invitation de %(targetName)s.",
|
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s a annulé l’invitation de %(targetName)s.",
|
||||||
"You are already in a call.": "Vous avez déjà un appel en cours.",
|
"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",
|
"There are no visible files in this room": "Il n'y a pas de fichier visible dans ce salon",
|
||||||
"Room": "Salon",
|
"Room": "Salon",
|
||||||
"Connectivity to the server has been lost.": "La connectivité au serveur a été perdue.",
|
"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",
|
"Cancel": "Annuler",
|
||||||
"Active call": "Appel en cours",
|
"Active call": "Appel en cours",
|
||||||
"code": "code",
|
"code": "code",
|
||||||
|
@ -774,5 +774,228 @@
|
||||||
"Failed to copy": "Échec de la copie",
|
"Failed to copy": "Échec de la copie",
|
||||||
"Verifies a user, device, and pubkey tuple": "Vérifie un utilisateur, un appareil et une clé publique",
|
"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",
|
"%(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
Loading…
Add table
Add a link
Reference in a new issue