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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default class ComposerHistoryManager {
|
||||||
|
|
||||||
// TODO: Performance issues?
|
// TODO: Performance issues?
|
||||||
let item;
|
let item;
|
||||||
for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
||||||
this.history.push(
|
this.history.push(
|
||||||
Object.assign(new HistoryItem(), JSON.parse(item)),
|
Object.assign(new HistoryItem(), JSON.parse(item)),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -84,7 +85,7 @@ class MatrixClientPeg {
|
||||||
if (this.matrixClient.initCrypto) {
|
if (this.matrixClient.initCrypto) {
|
||||||
await this.matrixClient.initCrypto();
|
await this.matrixClient.initCrypto();
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
// this can happen for a number of reasons, the most likely being
|
// this can happen for a number of reasons, the most likely being
|
||||||
// that the olm library was missing. It's not fatal.
|
// that the olm library was missing. It's not fatal.
|
||||||
console.warn("Unable to initialise e2e: " + e);
|
console.warn("Unable to initialise e2e: " + e);
|
||||||
|
@ -93,12 +94,13 @@ 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();
|
||||||
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
|
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
|
||||||
await promise;
|
await promise;
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
// log any errors when starting up the database (if one exists)
|
// log any errors when starting up the database (if one exists)
|
||||||
console.error(`Error starting matrixclient store: ${err}`);
|
console.error(`Error starting matrixclient store: ${err}`);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ function unicodeToEmojiUri(str) {
|
||||||
return unicodeChar;
|
return unicodeChar;
|
||||||
} else {
|
} else {
|
||||||
// Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below
|
// Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below
|
||||||
if(unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') {
|
if (unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') {
|
||||||
unicodeChar = unicodeChar[0];
|
unicodeChar = unicodeChar[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,9 +151,9 @@ function textForCallHangupEvent(event) {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
const eventContent = event.getContent();
|
const eventContent = event.getContent();
|
||||||
let reason = "";
|
let reason = "";
|
||||||
if(!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
reason = _t('(not supported by this browser)');
|
reason = _t('(not supported by this browser)');
|
||||||
} else if(eventContent.reason) {
|
} else if (eventContent.reason) {
|
||||||
if (eventContent.reason === "ice_failed") {
|
if (eventContent.reason === "ice_failed") {
|
||||||
reason = _t('(could not connect media)');
|
reason = _t('(could not connect media)');
|
||||||
} else if (eventContent.reason === "invite_timeout") {
|
} else if (eventContent.reason === "invite_timeout") {
|
||||||
|
|
480
src/Tinter.js
480
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,70 +15,288 @@ 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
|
||||||
// x * 118 + (1 - x) * 255 = 234
|
// x * 118 + (1 - x) * 255 = 234
|
||||||
// x * 118 + 255 - 255 * x = 234
|
// x * 118 + 255 - 255 * x = 234
|
||||||
// x * 118 - x * 255 = 234 - 255
|
// x * 118 - x * 255 = 234 - 255
|
||||||
// (255 - 118) x = 255 - 234
|
// (255 - 118) x = 255 - 234
|
||||||
// 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",
|
||||||
"borderTopColor",
|
"borderTopColor",
|
||||||
"borderBottomColor",
|
"borderBottomColor",
|
||||||
"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,10 +390,10 @@ 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) {
|
||||||
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
|
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
|
||||||
if (e.message) {
|
if (e.message) {
|
||||||
msg += e.message;
|
msg += e.message;
|
||||||
|
@ -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;
|
||||||
|
@ -126,7 +128,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
const events = this.room.getLiveTimeline().getEvents();
|
const events = this.room.getLiveTimeline().getEvents();
|
||||||
const lastSpoken = {};
|
const lastSpoken = {};
|
||||||
|
|
||||||
for(const event of events) {
|
for (const event of events) {
|
||||||
lastSpoken[event.getSender()] = event.getTs();
|
lastSpoken[event.getSender()] = event.getTs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
@ -301,7 +316,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// the first thing to do is to try the token params in the query-string
|
// the first thing to do is to try the token params in the query-string
|
||||||
Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => {
|
Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => {
|
||||||
if(loggedIn) {
|
if (loggedIn) {
|
||||||
this.props.onTokenLoginCompleted();
|
this.props.onTokenLoginCompleted();
|
||||||
|
|
||||||
// don't do anything else until the page reloads - just stay in
|
// don't do anything else until the page reloads - just stay in
|
||||||
|
@ -374,6 +389,22 @@ module.exports = React.createClass({
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const 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});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1147,7 +1104,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.searchScope === 'All') {
|
if (this.state.searchScope === 'All') {
|
||||||
if(roomId != lastRoomId) {
|
if (roomId != lastRoomId) {
|
||||||
const room = cli.getRoom(roomId);
|
const room = cli.getRoom(roomId);
|
||||||
|
|
||||||
// XXX: if we've left the room, we might not know about
|
// XXX: if we've left the room, we might not know about
|
||||||
|
@ -1458,13 +1415,13 @@ module.exports = React.createClass({
|
||||||
*/
|
*/
|
||||||
handleScrollKey: function(ev) {
|
handleScrollKey: function(ev) {
|
||||||
let panel;
|
let panel;
|
||||||
if(this.refs.searchResultsPanel) {
|
if (this.refs.searchResultsPanel) {
|
||||||
panel = this.refs.searchResultsPanel;
|
panel = this.refs.searchResultsPanel;
|
||||||
} else if(this.refs.messagePanel) {
|
} else if (this.refs.messagePanel) {
|
||||||
panel = this.refs.messagePanel;
|
panel = this.refs.messagePanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(panel) {
|
if (panel) {
|
||||||
panel.handleScrollKey(ev);
|
panel.handleScrollKey(ev);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1483,7 +1440,7 @@ module.exports = React.createClass({
|
||||||
// otherwise react calls it with null on each update.
|
// otherwise react calls it with null on each update.
|
||||||
_gatherTimelinePanelRef: function(r) {
|
_gatherTimelinePanelRef: function(r) {
|
||||||
this.refs.messagePanel = r;
|
this.refs.messagePanel = r;
|
||||||
if(r) {
|
if (r) {
|
||||||
console.log("updateTint from RoomView._gatherTimelinePanelRef");
|
console.log("updateTint from RoomView._gatherTimelinePanelRef");
|
||||||
this.updateTint();
|
this.updateTint();
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -573,7 +573,7 @@ module.exports = React.createClass({
|
||||||
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
|
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
|
||||||
pixelOffset + " (delta: "+scrollDelta+")");
|
pixelOffset + " (delta: "+scrollDelta+")");
|
||||||
|
|
||||||
if(scrollDelta != 0) {
|
if (scrollDelta != 0) {
|
||||||
this._setScrollTop(scrollNode.scrollTop + scrollDelta);
|
this._setScrollTop(scrollNode.scrollTop + scrollDelta);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
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"),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -311,7 +310,7 @@ var TimelinePanel = React.createClass({
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this._timelineWindow.canPaginate(dir)) {
|
if (!this._timelineWindow.canPaginate(dir)) {
|
||||||
debuglog("TimelinePanel: can't", dir, "paginate any further");
|
debuglog("TimelinePanel: can't", dir, "paginate any further");
|
||||||
this.setState({[canPaginateKey]: false});
|
this.setState({[canPaginateKey]: false});
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
|
@ -441,7 +440,7 @@ var TimelinePanel = React.createClass({
|
||||||
var callback = null;
|
var callback = null;
|
||||||
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
|
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
|
||||||
updatedState.readMarkerVisible = true;
|
updatedState.readMarkerVisible = true;
|
||||||
} else if(lastEv && this.getReadMarkerPosition() === 0) {
|
} else if (lastEv && this.getReadMarkerPosition() === 0) {
|
||||||
// we know we're stuckAtBottom, so we can advance the RM
|
// we know we're stuckAtBottom, so we can advance the RM
|
||||||
// immediately, to save a later render cycle
|
// immediately, to save a later render cycle
|
||||||
|
|
||||||
|
@ -658,7 +657,7 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// the read-marker should become invisible, so that if the user scrolls
|
// the read-marker should become invisible, so that if the user scrolls
|
||||||
// down, they don't see it.
|
// down, they don't see it.
|
||||||
if(this.state.readMarkerVisible) {
|
if (this.state.readMarkerVisible) {
|
||||||
this.setState({
|
this.setState({
|
||||||
readMarkerVisible: false,
|
readMarkerVisible: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -691,8 +622,8 @@ 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();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -95,7 +104,7 @@ module.exports = React.createClass({
|
||||||
).then((data) => {
|
).then((data) => {
|
||||||
this.props.onLoggedIn(data);
|
this.props.onLoggedIn(data);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
if(this._unmounted) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let errorText;
|
let errorText;
|
||||||
|
@ -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);
|
||||||
|
@ -120,7 +144,7 @@ module.exports = React.createClass({
|
||||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
|
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
|
||||||
});
|
});
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
if(this._unmounted) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -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">
|
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||||
{ _t("Are you sure you wish to remove (delete) this event? " +
|
button={_t("Remove")}>
|
||||||
"Note that if you delete a room name or topic change, it could undo the change.") }
|
</QuestionDialog>
|
||||||
</div>
|
|
||||||
<div className="mx_Dialog_buttons">
|
|
||||||
<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 (
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default React.createClass({
|
||||||
|
|
||||||
const deviceInfo = r[userId][deviceId];
|
const deviceInfo = r[userId][deviceId];
|
||||||
|
|
||||||
if(!deviceInfo) {
|
if (!deviceInfo) {
|
||||||
console.warn(`No details found for device ${userId}:${deviceId}`);
|
console.warn(`No details found for device ${userId}:${deviceId}`);
|
||||||
|
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -19,9 +19,9 @@ export default class AppPermission extends React.Component {
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(wurl.search);
|
const searchParams = new URLSearchParams(wurl.search);
|
||||||
|
|
||||||
if(this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
if (this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||||
curl = url.parse(searchParams.get('url'));
|
curl = url.parse(searchParams.get('url'));
|
||||||
if(curl) {
|
if (curl) {
|
||||||
curl.search = curl.query = "";
|
curl.search = curl.query = "";
|
||||||
curlString = curl.format();
|
curlString = curl.format();
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ export default class AppPermission extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
isScalarWurl(wurl) {
|
isScalarWurl(wurl) {
|
||||||
if(wurl && wurl.hostname && (
|
if (wurl && wurl.hostname && (
|
||||||
wurl.hostname === 'scalar.vector.im' ||
|
wurl.hostname === 'scalar.vector.im' ||
|
||||||
wurl.hostname === 'scalar-staging.riot.im' ||
|
wurl.hostname === 'scalar-staging.riot.im' ||
|
||||||
wurl.hostname === 'scalar-develop.riot.im' ||
|
wurl.hostname === 'scalar-develop.riot.im' ||
|
||||||
|
|
|
@ -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,15 +329,15 @@ 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();
|
||||||
}
|
}
|
||||||
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,9 +423,9 @@ 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;
|
||||||
|
@ -41,8 +41,8 @@ export default class LanguageDropdown extends React.Component {
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
||||||
langs.sort(function(a, b) {
|
langs.sort(function(a, b) {
|
||||||
if(a.label < b.label) return -1;
|
if (a.label < b.label) return -1;
|
||||||
if(a.label > b.label) return 1;
|
if (a.label > b.label) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
this.setState({langs});
|
this.setState({langs});
|
||||||
|
@ -54,10 +54,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -216,7 +216,7 @@ module.exports = React.createClass({
|
||||||
// are there only to show translators to non-English languages
|
// are there only to show translators to non-English languages
|
||||||
// that the verb is conjugated to plural or singular Subject.
|
// that the verb is conjugated to plural or singular Subject.
|
||||||
let res = null;
|
let res = null;
|
||||||
switch(t) {
|
switch (t) {
|
||||||
case "joined":
|
case "joined":
|
||||||
res = (userCount > 1)
|
res = (userCount > 1)
|
||||||
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats })
|
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats })
|
||||||
|
@ -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.
|
||||||
|
@ -122,7 +122,7 @@ class PasswordLogin extends React.Component {
|
||||||
mx_Login_field_disabled: disabled,
|
mx_Login_field_disabled: disabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch(loginType) {
|
switch (loginType) {
|
||||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||||
classes.mx_Login_email = true;
|
classes.mx_Login_email = true;
|
||||||
return <input
|
return <input
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -133,7 +133,7 @@ module.exports = React.createClass({
|
||||||
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||||
};
|
};
|
||||||
|
|
||||||
if(app.data) {
|
if (app.data) {
|
||||||
Object.keys(app.data).forEach((key) => {
|
Object.keys(app.data).forEach((key) => {
|
||||||
params['$' + key] = app.data[key];
|
params['$' + key] = app.data[key];
|
||||||
});
|
});
|
||||||
|
@ -177,7 +177,7 @@ module.exports = React.createClass({
|
||||||
_canUserModify: function() {
|
_canUserModify: function() {
|
||||||
try {
|
try {
|
||||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -564,7 +562,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
onMemberAvatarClick: function() {
|
onMemberAvatarClick: function() {
|
||||||
const member = this.props.member;
|
const member = this.props.member;
|
||||||
const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url;
|
const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url;
|
||||||
if(!avatarUrl) return;
|
if (!avatarUrl) return;
|
||||||
|
|
||||||
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
|
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
|
||||||
const ImageView = sdk.getComponent("elements.ImageView");
|
const ImageView = sdk.getComponent("elements.ImageView");
|
||||||
|
@ -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'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,10 +111,10 @@ export default class MessageComposer extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
onFinished: (shouldUpload) => {
|
onFinished: (shouldUpload) => {
|
||||||
if(shouldUpload) {
|
if (shouldUpload) {
|
||||||
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
|
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
|
||||||
if (files) {
|
if (files) {
|
||||||
for(let i=0; i<files.length; i++) {
|
for (let i=0; i<files.length; i++) {
|
||||||
this.props.uploadFile(files[i]);
|
this.props.uploadFile(files[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,10 +425,10 @@ 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) {
|
||||||
// plaintext -> hex unicode
|
// plaintext -> hex unicode
|
||||||
const emojiUc = asciiList[emojiMatch[1]];
|
const emojiUc = asciiList[emojiMatch[1]];
|
||||||
// hex unicode -> shortname -> actual unicode
|
// hex unicode -> shortname -> actual unicode
|
||||||
|
@ -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 => {
|
||||||
|
@ -696,7 +690,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState);
|
const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState);
|
||||||
if(
|
if (
|
||||||
['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item']
|
['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item']
|
||||||
.includes(currentBlockType)
|
.includes(currentBlockType)
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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" />);
|
||||||
|
@ -389,7 +389,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
let rightRow;
|
let rightRow;
|
||||||
let manageIntegsButton;
|
let manageIntegsButton;
|
||||||
if(this.props.room && this.props.room.roomId && this.props.inRoom) {
|
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
|
||||||
manageIntegsButton = <ManageIntegsButton
|
manageIntegsButton = <ManageIntegsButton
|
||||||
roomId={this.props.room.roomId}
|
roomId={this.props.room.roomId}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -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 }
|
||||||
|
@ -165,13 +165,13 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
let actionText;
|
let actionText;
|
||||||
if (kicked) {
|
if (kicked) {
|
||||||
if(roomName) {
|
if (roomName) {
|
||||||
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
|
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
|
||||||
} else {
|
} else {
|
||||||
actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName});
|
actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName});
|
||||||
}
|
}
|
||||||
} else if (banned) {
|
} else if (banned) {
|
||||||
if(roomName) {
|
if (roomName) {
|
||||||
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
|
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
|
||||||
} else {
|
} else {
|
||||||
actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName});
|
actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName});
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -55,7 +54,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return({
|
return ({
|
||||||
hover: false,
|
hover: false,
|
||||||
badgeHover: false,
|
badgeHover: false,
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
|
|
|
@ -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() {},
|
||||||
};
|
};
|
||||||
|
|
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