Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into set_default_federate_by_settings
This commit is contained in:
commit
7492f2dffa
149 changed files with 7702 additions and 5253 deletions
|
@ -6,7 +6,6 @@ src/autocomplete/Autocompleter.js
|
||||||
src/autocomplete/Components.js
|
src/autocomplete/Components.js
|
||||||
src/autocomplete/DuckDuckGoProvider.js
|
src/autocomplete/DuckDuckGoProvider.js
|
||||||
src/autocomplete/EmojiProvider.js
|
src/autocomplete/EmojiProvider.js
|
||||||
src/autocomplete/RoomProvider.js
|
|
||||||
src/autocomplete/UserProvider.js
|
src/autocomplete/UserProvider.js
|
||||||
src/CallHandler.js
|
src/CallHandler.js
|
||||||
src/component-index.js
|
src/component-index.js
|
||||||
|
@ -35,7 +34,6 @@ src/components/views/create_room/RoomAlias.js
|
||||||
src/components/views/dialogs/ChatCreateOrReuseDialog.js
|
src/components/views/dialogs/ChatCreateOrReuseDialog.js
|
||||||
src/components/views/dialogs/DeactivateAccountDialog.js
|
src/components/views/dialogs/DeactivateAccountDialog.js
|
||||||
src/components/views/dialogs/InteractiveAuthDialog.js
|
src/components/views/dialogs/InteractiveAuthDialog.js
|
||||||
src/components/views/dialogs/SetMxIdDialog.js
|
|
||||||
src/components/views/dialogs/UnknownDeviceDialog.js
|
src/components/views/dialogs/UnknownDeviceDialog.js
|
||||||
src/components/views/elements/AccessibleButton.js
|
src/components/views/elements/AccessibleButton.js
|
||||||
src/components/views/elements/ActionButton.js
|
src/components/views/elements/ActionButton.js
|
||||||
|
@ -56,7 +54,6 @@ src/components/views/elements/RoomDirectoryButton.js
|
||||||
src/components/views/elements/SettingsButton.js
|
src/components/views/elements/SettingsButton.js
|
||||||
src/components/views/elements/StartChatButton.js
|
src/components/views/elements/StartChatButton.js
|
||||||
src/components/views/elements/TintableSvg.js
|
src/components/views/elements/TintableSvg.js
|
||||||
src/components/views/elements/TruncatedList.js
|
|
||||||
src/components/views/elements/UserSelector.js
|
src/components/views/elements/UserSelector.js
|
||||||
src/components/views/login/CaptchaForm.js
|
src/components/views/login/CaptchaForm.js
|
||||||
src/components/views/login/CasLogin.js
|
src/components/views/login/CasLogin.js
|
||||||
|
@ -89,7 +86,6 @@ src/components/views/rooms/MemberList.js
|
||||||
src/components/views/rooms/MemberTile.js
|
src/components/views/rooms/MemberTile.js
|
||||||
src/components/views/rooms/MessageComposer.js
|
src/components/views/rooms/MessageComposer.js
|
||||||
src/components/views/rooms/MessageComposerInput.js
|
src/components/views/rooms/MessageComposerInput.js
|
||||||
src/components/views/rooms/MessageComposerInputOld.js
|
|
||||||
src/components/views/rooms/PresenceLabel.js
|
src/components/views/rooms/PresenceLabel.js
|
||||||
src/components/views/rooms/ReadReceiptMarker.js
|
src/components/views/rooms/ReadReceiptMarker.js
|
||||||
src/components/views/rooms/RoomList.js
|
src/components/views/rooms/RoomList.js
|
||||||
|
@ -100,7 +96,6 @@ src/components/views/rooms/RoomTile.js
|
||||||
src/components/views/rooms/RoomTopicEditor.js
|
src/components/views/rooms/RoomTopicEditor.js
|
||||||
src/components/views/rooms/SearchableEntityList.js
|
src/components/views/rooms/SearchableEntityList.js
|
||||||
src/components/views/rooms/SearchResultTile.js
|
src/components/views/rooms/SearchResultTile.js
|
||||||
src/components/views/rooms/TabCompleteBar.js
|
|
||||||
src/components/views/rooms/TopUnreadMessagesBar.js
|
src/components/views/rooms/TopUnreadMessagesBar.js
|
||||||
src/components/views/rooms/UserTile.js
|
src/components/views/rooms/UserTile.js
|
||||||
src/components/views/settings/AddPhoneNumber.js
|
src/components/views/settings/AddPhoneNumber.js
|
||||||
|
@ -128,9 +123,6 @@ src/Roles.js
|
||||||
src/Rooms.js
|
src/Rooms.js
|
||||||
src/ScalarAuthClient.js
|
src/ScalarAuthClient.js
|
||||||
src/ScalarMessaging.js
|
src/ScalarMessaging.js
|
||||||
src/TabComplete.js
|
|
||||||
src/TabCompleteEntries.js
|
|
||||||
src/TextForEvent.js
|
|
||||||
src/Tinter.js
|
src/Tinter.js
|
||||||
src/UiEffects.js
|
src/UiEffects.js
|
||||||
src/Unread.js
|
src/Unread.js
|
||||||
|
@ -142,7 +134,7 @@ src/utils/Receipt.js
|
||||||
src/Velociraptor.js
|
src/Velociraptor.js
|
||||||
src/VelocityBounce.js
|
src/VelocityBounce.js
|
||||||
src/WhoIsTyping.js
|
src/WhoIsTyping.js
|
||||||
src/wrappers/WithMatrixClient.js
|
src/wrappers/withMatrixClient.js
|
||||||
test/all-tests.js
|
test/all-tests.js
|
||||||
test/components/structures/login/Registration-test.js
|
test/components/structures/login/Registration-test.js
|
||||||
test/components/structures/MessagePanel-test.js
|
test/components/structures/MessagePanel-test.js
|
||||||
|
|
13
.eslintrc.js
13
.eslintrc.js
|
@ -40,6 +40,19 @@ module.exports = {
|
||||||
}],
|
}],
|
||||||
"react/jsx-key": ["error"],
|
"react/jsx-key": ["error"],
|
||||||
|
|
||||||
|
// Assert no spacing in JSX curly brackets
|
||||||
|
// <Element prop={ consideredError} prop={notConsideredError} />
|
||||||
|
//
|
||||||
|
// https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-curly-spacing.md
|
||||||
|
"react/jsx-curly-spacing": ["error", {"when": "never", "children": {"when": "always"}}],
|
||||||
|
|
||||||
|
// Assert spacing before self-closing JSX tags, and no spacing before or
|
||||||
|
// after the closing slash, and no spacing after the opening bracket of
|
||||||
|
// the opening tag or closing tag.
|
||||||
|
//
|
||||||
|
// https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-tag-spacing.md
|
||||||
|
"react/jsx-tag-spacing": ["error"],
|
||||||
|
|
||||||
/** flowtype **/
|
/** flowtype **/
|
||||||
"flowtype/require-parameter-type": ["warn", {
|
"flowtype/require-parameter-type": ["warn", {
|
||||||
"excludeArrowFunctions": true,
|
"excludeArrowFunctions": true,
|
||||||
|
|
226
CHANGELOG.md
226
CHANGELOG.md
|
@ -1,3 +1,229 @@
|
||||||
|
Changes in [0.10.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.6) (2017-09-21)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.5...v0.10.6)
|
||||||
|
|
||||||
|
* New version of js-sdk with fixed build
|
||||||
|
|
||||||
|
Changes in [0.10.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.5) (2017-09-21)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.4...v0.10.5)
|
||||||
|
|
||||||
|
* Fix build error (https://github.com/vector-im/riot-web/issues/5091)
|
||||||
|
|
||||||
|
Changes in [0.10.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.4) (2017-09-20)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.4-rc.1...v0.10.4)
|
||||||
|
|
||||||
|
* No changes
|
||||||
|
|
||||||
|
Changes in [0.10.4-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.4-rc.1) (2017-09-19)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3...v0.10.4-rc.1)
|
||||||
|
|
||||||
|
* Fix RoomView stuck in 'accept invite' state
|
||||||
|
[\#1396](https://github.com/matrix-org/matrix-react-sdk/pull/1396)
|
||||||
|
* Only show the integ management button if user is joined
|
||||||
|
[\#1398](https://github.com/matrix-org/matrix-react-sdk/pull/1398)
|
||||||
|
* suppressOnHover for member entity tiles which have no onClick
|
||||||
|
[\#1273](https://github.com/matrix-org/matrix-react-sdk/pull/1273)
|
||||||
|
* add /devtools command
|
||||||
|
[\#1268](https://github.com/matrix-org/matrix-react-sdk/pull/1268)
|
||||||
|
* Fix broken Link
|
||||||
|
[\#1359](https://github.com/matrix-org/matrix-react-sdk/pull/1359)
|
||||||
|
* Show who redacted an event on hover
|
||||||
|
[\#1387](https://github.com/matrix-org/matrix-react-sdk/pull/1387)
|
||||||
|
* start MELS expanded if it contains a highlighted/permalinked event.
|
||||||
|
[\#1388](https://github.com/matrix-org/matrix-react-sdk/pull/1388)
|
||||||
|
* Add ignore user API support
|
||||||
|
[\#1389](https://github.com/matrix-org/matrix-react-sdk/pull/1389)
|
||||||
|
* Add option to disable Emoji suggestions
|
||||||
|
[\#1392](https://github.com/matrix-org/matrix-react-sdk/pull/1392)
|
||||||
|
* sanitize the i18n for fn:textForHistoryVisibilityEvent
|
||||||
|
[\#1397](https://github.com/matrix-org/matrix-react-sdk/pull/1397)
|
||||||
|
* Don't check for only-emoji if there were none
|
||||||
|
[\#1394](https://github.com/matrix-org/matrix-react-sdk/pull/1394)
|
||||||
|
* Fix emojification of symbol characters
|
||||||
|
[\#1393](https://github.com/matrix-org/matrix-react-sdk/pull/1393)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1395](https://github.com/matrix-org/matrix-react-sdk/pull/1395)
|
||||||
|
* Make /join join again
|
||||||
|
[\#1391](https://github.com/matrix-org/matrix-react-sdk/pull/1391)
|
||||||
|
* Display spinner not room preview after room create
|
||||||
|
[\#1390](https://github.com/matrix-org/matrix-react-sdk/pull/1390)
|
||||||
|
* Fix the avatar / room name in room preview
|
||||||
|
[\#1384](https://github.com/matrix-org/matrix-react-sdk/pull/1384)
|
||||||
|
* Remove spurious cancel button
|
||||||
|
[\#1381](https://github.com/matrix-org/matrix-react-sdk/pull/1381)
|
||||||
|
* Fix starting a chat by email address
|
||||||
|
[\#1386](https://github.com/matrix-org/matrix-react-sdk/pull/1386)
|
||||||
|
* respond on copy code block
|
||||||
|
[\#1363](https://github.com/matrix-org/matrix-react-sdk/pull/1363)
|
||||||
|
* fix DateUtils inconsistency with 12/24h
|
||||||
|
[\#1383](https://github.com/matrix-org/matrix-react-sdk/pull/1383)
|
||||||
|
* allow sending sub,sup and whitelist them on receive
|
||||||
|
[\#1382](https://github.com/matrix-org/matrix-react-sdk/pull/1382)
|
||||||
|
* Update roomlist when an event is decrypted
|
||||||
|
[\#1380](https://github.com/matrix-org/matrix-react-sdk/pull/1380)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1379](https://github.com/matrix-org/matrix-react-sdk/pull/1379)
|
||||||
|
* fix radio for theme selection
|
||||||
|
[\#1368](https://github.com/matrix-org/matrix-react-sdk/pull/1368)
|
||||||
|
* fix some more zh_Hans - remove entirely broken lines
|
||||||
|
[\#1378](https://github.com/matrix-org/matrix-react-sdk/pull/1378)
|
||||||
|
* fix placeholder causing app to break when using zh
|
||||||
|
[\#1377](https://github.com/matrix-org/matrix-react-sdk/pull/1377)
|
||||||
|
* Avoid re-rendering RoomList on room switch
|
||||||
|
[\#1375](https://github.com/matrix-org/matrix-react-sdk/pull/1375)
|
||||||
|
* Fix 'Failed to load timeline position' regression
|
||||||
|
[\#1376](https://github.com/matrix-org/matrix-react-sdk/pull/1376)
|
||||||
|
* Fast path for emojifying strings
|
||||||
|
[\#1372](https://github.com/matrix-org/matrix-react-sdk/pull/1372)
|
||||||
|
* Consolidate the code copy button
|
||||||
|
[\#1374](https://github.com/matrix-org/matrix-react-sdk/pull/1374)
|
||||||
|
* Only add the code copy button for HTML messages
|
||||||
|
[\#1373](https://github.com/matrix-org/matrix-react-sdk/pull/1373)
|
||||||
|
* Don't re-render matrixchat unnecessarily
|
||||||
|
[\#1371](https://github.com/matrix-org/matrix-react-sdk/pull/1371)
|
||||||
|
* Don't wait for setState to run onHaveRoom
|
||||||
|
[\#1370](https://github.com/matrix-org/matrix-react-sdk/pull/1370)
|
||||||
|
* Introduce a RoomScrollStateStore
|
||||||
|
[\#1367](https://github.com/matrix-org/matrix-react-sdk/pull/1367)
|
||||||
|
* Don't always paginate when mounting a ScrollPanel
|
||||||
|
[\#1369](https://github.com/matrix-org/matrix-react-sdk/pull/1369)
|
||||||
|
* Remove unused scrollStateMap from LoggedinView
|
||||||
|
[\#1366](https://github.com/matrix-org/matrix-react-sdk/pull/1366)
|
||||||
|
* Revert "Implement sticky date separators"
|
||||||
|
[\#1365](https://github.com/matrix-org/matrix-react-sdk/pull/1365)
|
||||||
|
* Remove unused string "changing room on a RoomView is not supported"
|
||||||
|
[\#1361](https://github.com/matrix-org/matrix-react-sdk/pull/1361)
|
||||||
|
* Remove unused translation code translations
|
||||||
|
[\#1360](https://github.com/matrix-org/matrix-react-sdk/pull/1360)
|
||||||
|
* Implement sticky date separators
|
||||||
|
[\#1353](https://github.com/matrix-org/matrix-react-sdk/pull/1353)
|
||||||
|
|
||||||
|
Changes in [0.10.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3) (2017-09-06)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3-rc.2...v0.10.3)
|
||||||
|
|
||||||
|
* No changes
|
||||||
|
|
||||||
|
Changes in [0.10.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3-rc.2) (2017-09-05)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3-rc.1...v0.10.3-rc.2)
|
||||||
|
|
||||||
|
* Fix plurals in translations
|
||||||
|
[\#1358](https://github.com/matrix-org/matrix-react-sdk/pull/1358)
|
||||||
|
* Fix typo
|
||||||
|
[\#1357](https://github.com/matrix-org/matrix-react-sdk/pull/1357)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1356](https://github.com/matrix-org/matrix-react-sdk/pull/1356)
|
||||||
|
|
||||||
|
Changes in [0.10.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3-rc.1) (2017-09-01)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.2...v0.10.3-rc.1)
|
||||||
|
|
||||||
|
* Fix room change sometimes being very slow
|
||||||
|
[\#1354](https://github.com/matrix-org/matrix-react-sdk/pull/1354)
|
||||||
|
* apply shouldHideEvent fn to onRoomTimeline for RoomStatusBar
|
||||||
|
[\#1346](https://github.com/matrix-org/matrix-react-sdk/pull/1346)
|
||||||
|
* text4event widget modified, used to show widget added each time.
|
||||||
|
[\#1345](https://github.com/matrix-org/matrix-react-sdk/pull/1345)
|
||||||
|
* separate concepts of showing and managing RRs to fix regression
|
||||||
|
[\#1352](https://github.com/matrix-org/matrix-react-sdk/pull/1352)
|
||||||
|
* Make staging widgets work with live and vice versa.
|
||||||
|
[\#1350](https://github.com/matrix-org/matrix-react-sdk/pull/1350)
|
||||||
|
* Avoid breaking /sync with uncaught exceptions
|
||||||
|
[\#1349](https://github.com/matrix-org/matrix-react-sdk/pull/1349)
|
||||||
|
* we need to pass whether it is an invite RoomSubList explicitly (i18n)
|
||||||
|
[\#1343](https://github.com/matrix-org/matrix-react-sdk/pull/1343)
|
||||||
|
* Percent encoding isn't a valid thing within _t
|
||||||
|
[\#1348](https://github.com/matrix-org/matrix-react-sdk/pull/1348)
|
||||||
|
* Fix spurious notifications
|
||||||
|
[\#1339](https://github.com/matrix-org/matrix-react-sdk/pull/1339)
|
||||||
|
* Unbreak password reset with a non-default HS
|
||||||
|
[\#1347](https://github.com/matrix-org/matrix-react-sdk/pull/1347)
|
||||||
|
* Remove unnecessary 'load' on notif audio element
|
||||||
|
[\#1341](https://github.com/matrix-org/matrix-react-sdk/pull/1341)
|
||||||
|
* _tJsx returns a React Object, the sub fn must return a React Object
|
||||||
|
[\#1340](https://github.com/matrix-org/matrix-react-sdk/pull/1340)
|
||||||
|
* Fix deprecation warning about promise.defer()
|
||||||
|
[\#1292](https://github.com/matrix-org/matrix-react-sdk/pull/1292)
|
||||||
|
* Fix click to insert completion
|
||||||
|
[\#1331](https://github.com/matrix-org/matrix-react-sdk/pull/1331)
|
||||||
|
|
||||||
|
Changes in [0.10.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.2) (2017-08-24)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.1...v0.10.2)
|
||||||
|
|
||||||
|
* Force update on timelinepanel when event decrypted
|
||||||
|
[\#1334](https://github.com/matrix-org/matrix-react-sdk/pull/1334)
|
||||||
|
* Dispatch incoming_call synchronously
|
||||||
|
[\#1337](https://github.com/matrix-org/matrix-react-sdk/pull/1337)
|
||||||
|
* Fix React crying on machines without internet due to return undefined
|
||||||
|
[\#1335](https://github.com/matrix-org/matrix-react-sdk/pull/1335)
|
||||||
|
* Catch the promise rejection if scalar fails
|
||||||
|
[\#1333](https://github.com/matrix-org/matrix-react-sdk/pull/1333)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1329](https://github.com/matrix-org/matrix-react-sdk/pull/1329)
|
||||||
|
|
||||||
|
Changes in [0.10.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.1) (2017-08-23)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.1-rc.1...v0.10.1)
|
||||||
|
|
||||||
|
* [No changes]
|
||||||
|
|
||||||
|
Changes in [0.10.1-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.1-rc.1) (2017-08-22)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.0-rc.2...v0.10.1-rc.1)
|
||||||
|
|
||||||
|
* Matthew/multiple widgets
|
||||||
|
[\#1327](https://github.com/matrix-org/matrix-react-sdk/pull/1327)
|
||||||
|
* Fix proptypes on UserPickerDialog
|
||||||
|
[\#1326](https://github.com/matrix-org/matrix-react-sdk/pull/1326)
|
||||||
|
* AppsDrawer: Remove unnecessary bind
|
||||||
|
[\#1325](https://github.com/matrix-org/matrix-react-sdk/pull/1325)
|
||||||
|
* Position add app widget link
|
||||||
|
[\#1322](https://github.com/matrix-org/matrix-react-sdk/pull/1322)
|
||||||
|
* Remove app tile beta tag.
|
||||||
|
[\#1323](https://github.com/matrix-org/matrix-react-sdk/pull/1323)
|
||||||
|
* Add missing translation.
|
||||||
|
[\#1324](https://github.com/matrix-org/matrix-react-sdk/pull/1324)
|
||||||
|
* Note that apps are not E2EE
|
||||||
|
[\#1319](https://github.com/matrix-org/matrix-react-sdk/pull/1319)
|
||||||
|
* Only render appTile body (including warnings) if drawer shown.
|
||||||
|
[\#1321](https://github.com/matrix-org/matrix-react-sdk/pull/1321)
|
||||||
|
* Timeline improvements
|
||||||
|
[\#1320](https://github.com/matrix-org/matrix-react-sdk/pull/1320)
|
||||||
|
* Add a space between widget name and "widget" in widget event tiles
|
||||||
|
[\#1318](https://github.com/matrix-org/matrix-react-sdk/pull/1318)
|
||||||
|
* Move manage integrations button from settings page to room header as a
|
||||||
|
stand-alone component
|
||||||
|
[\#1286](https://github.com/matrix-org/matrix-react-sdk/pull/1286)
|
||||||
|
* Don't apply case logic to app names
|
||||||
|
[\#1316](https://github.com/matrix-org/matrix-react-sdk/pull/1316)
|
||||||
|
* Stop integ manager opening on every room switch
|
||||||
|
[\#1315](https://github.com/matrix-org/matrix-react-sdk/pull/1315)
|
||||||
|
* Add behaviour to toggle app draw on app tile header click
|
||||||
|
[\#1313](https://github.com/matrix-org/matrix-react-sdk/pull/1313)
|
||||||
|
* Change OOO so that MELS generation will continue over hidden events
|
||||||
|
[\#1308](https://github.com/matrix-org/matrix-react-sdk/pull/1308)
|
||||||
|
* Implement TextualEvent tiles for im.vector.modular.widgets
|
||||||
|
[\#1312](https://github.com/matrix-org/matrix-react-sdk/pull/1312)
|
||||||
|
* Don't show widget security warning to the person that added it to the room
|
||||||
|
[\#1314](https://github.com/matrix-org/matrix-react-sdk/pull/1314)
|
||||||
|
* remove unused strings introduced by string change
|
||||||
|
[\#1311](https://github.com/matrix-org/matrix-react-sdk/pull/1311)
|
||||||
|
* hotfix bad fn signature regression
|
||||||
|
[\#1310](https://github.com/matrix-org/matrix-react-sdk/pull/1310)
|
||||||
|
* Show a dialog if the maximum number of widgets allowed has been reached.
|
||||||
|
[\#1291](https://github.com/matrix-org/matrix-react-sdk/pull/1291)
|
||||||
|
* Fix Robot translation
|
||||||
|
[\#1309](https://github.com/matrix-org/matrix-react-sdk/pull/1309)
|
||||||
|
* Refactor ChatInviteDialog to be UserPickerDialog
|
||||||
|
[\#1300](https://github.com/matrix-org/matrix-react-sdk/pull/1300)
|
||||||
|
* Update Link to Translation status
|
||||||
|
[\#1302](https://github.com/matrix-org/matrix-react-sdk/pull/1302)
|
||||||
|
|
||||||
Changes in [0.9.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.7) (2017-06-22)
|
Changes in [0.9.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.7) (2017-06-22)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.6...v0.9.7)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.6...v0.9.7)
|
||||||
|
|
|
@ -46,7 +46,7 @@ Please follow the standard Matrix contributor's guide:
|
||||||
https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst
|
https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst
|
||||||
|
|
||||||
Please follow the Matrix JS/React code style as per:
|
Please follow the Matrix JS/React code style as per:
|
||||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/code_style.rst
|
https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md
|
||||||
|
|
||||||
Whilst the layering separation between matrix-react-sdk and Riot is broken
|
Whilst the layering separation between matrix-react-sdk and Riot is broken
|
||||||
(as of July 2016), code should be committed as follows:
|
(as of July 2016), code should be committed as follows:
|
||||||
|
|
|
@ -21,9 +21,7 @@ npm run test -- --no-colors
|
||||||
npm run lintall -- -f checkstyle -o eslint.xml || true
|
npm run lintall -- -f checkstyle -o eslint.xml || true
|
||||||
|
|
||||||
# re-run the linter, excluding any files known to have errors or warnings.
|
# re-run the linter, excluding any files known to have errors or warnings.
|
||||||
./node_modules/.bin/eslint --max-warnings 0 \
|
npm run lintwithexclusions
|
||||||
--ignore-path .eslintignore.errorfiles \
|
|
||||||
src test
|
|
||||||
|
|
||||||
# delete the old tarball, if it exists
|
# delete the old tarball, if it exists
|
||||||
rm -f matrix-react-sdk-*.tgz
|
rm -f matrix-react-sdk-*.tgz
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.9.7",
|
"version": "0.10.6",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -39,8 +39,9 @@
|
||||||
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lintall": "eslint src/ test/",
|
"lintall": "eslint src/ test/",
|
||||||
|
"lintwithexclusions": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
|
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt",
|
||||||
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
||||||
"test-multi": "karma start"
|
"test-multi": "karma start"
|
||||||
},
|
},
|
||||||
|
@ -66,7 +67,7 @@
|
||||||
"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": "matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "0.8.4",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
|
@ -99,7 +100,7 @@
|
||||||
"eslint-config-google": "^0.7.1",
|
"eslint-config-google": "^0.7.1",
|
||||||
"eslint-plugin-babel": "^4.0.1",
|
"eslint-plugin-babel": "^4.0.1",
|
||||||
"eslint-plugin-flowtype": "^2.30.0",
|
"eslint-plugin-flowtype": "^2.30.0",
|
||||||
"eslint-plugin-react": "^6.9.0",
|
"eslint-plugin-react": "^7.4.0",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
"json-loader": "^0.5.3",
|
"json-loader": "^0.5.3",
|
||||||
"karma": "^1.7.0",
|
"karma": "^1.7.0",
|
||||||
|
|
|
@ -6,6 +6,4 @@ npm run test
|
||||||
./.travis-test-riot.sh
|
./.travis-test-riot.sh
|
||||||
|
|
||||||
# run the linter, but exclude any files known to have errors or warnings.
|
# run the linter, but exclude any files known to have errors or warnings.
|
||||||
./node_modules/.bin/eslint --max-warnings 0 \
|
npm run lintwithexclusions
|
||||||
--ignore-path .eslintignore.errorfiles \
|
|
||||||
src test
|
|
||||||
|
|
77
src/ActiveRoomObserver.js
Normal file
77
src/ActiveRoomObserver.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
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 RoomViewStore from './stores/RoomViewStore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes changes from the RoomViewStore and notifies specific things
|
||||||
|
* about when the active room changes. Unlike listening for RoomViewStore
|
||||||
|
* changes, you can subscribe to only changes relevant to a particular
|
||||||
|
* room.
|
||||||
|
*
|
||||||
|
* TODO: If we introduce an observer for something else, factor out
|
||||||
|
* the adding / removing of listeners & emitting into a common class.
|
||||||
|
*/
|
||||||
|
class ActiveRoomObserver {
|
||||||
|
constructor() {
|
||||||
|
this._listeners = {};
|
||||||
|
|
||||||
|
this._activeRoomId = RoomViewStore.getRoomId();
|
||||||
|
// TODO: We could self-destruct when the last listener goes away, or at least
|
||||||
|
// stop listening.
|
||||||
|
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(roomId, listener) {
|
||||||
|
if (!this._listeners[roomId]) this._listeners[roomId] = [];
|
||||||
|
this._listeners[roomId].push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener(roomId, listener) {
|
||||||
|
if (this._listeners[roomId]) {
|
||||||
|
const i = this._listeners[roomId].indexOf(listener);
|
||||||
|
if (i > -1) {
|
||||||
|
this._listeners[roomId].splice(i, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Unregistering unrecognised listener (roomId=" + roomId + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_emit(roomId) {
|
||||||
|
if (!this._listeners[roomId]) return;
|
||||||
|
|
||||||
|
for (const l of this._listeners[roomId]) {
|
||||||
|
l.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRoomViewStoreUpdate() {
|
||||||
|
// emit for the old room ID
|
||||||
|
if (this._activeRoomId) this._emit(this._activeRoomId);
|
||||||
|
|
||||||
|
// update our cache
|
||||||
|
this._activeRoomId = RoomViewStore.getRoomId();
|
||||||
|
|
||||||
|
// and emit for the new one
|
||||||
|
if (this._activeRoomId) this._emit(this._activeRoomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.mx_ActiveRoomObserver === undefined) {
|
||||||
|
global.mx_ActiveRoomObserver = new ActiveRoomObserver();
|
||||||
|
}
|
||||||
|
export default global.mx_ActiveRoomObserver;
|
|
@ -107,6 +107,9 @@ export default class BasePlatform {
|
||||||
|
|
||||||
isElectron(): boolean { return false; }
|
isElectron(): boolean { return false; }
|
||||||
|
|
||||||
|
setupScreenSharingForIframe() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restarts the application, without neccessarily reloading
|
* Restarts the application, without neccessarily reloading
|
||||||
* any application code
|
* any application code
|
||||||
|
|
|
@ -65,7 +65,7 @@ module.exports = {
|
||||||
const days = getDaysArray();
|
const days = getDaysArray();
|
||||||
const months = getMonthsArray();
|
const months = getMonthsArray();
|
||||||
if (date.toDateString() === now.toDateString()) {
|
if (date.toDateString() === now.toDateString()) {
|
||||||
return this.formatTime(date);
|
return this.formatTime(date, showTwelveHour);
|
||||||
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||||
// TODO: use standard date localize function provided in counterpart
|
// TODO: use standard date localize function provided in counterpart
|
||||||
return _t('%(weekDayName)s %(time)s', {
|
return _t('%(weekDayName)s %(time)s', {
|
||||||
|
@ -78,7 +78,7 @@ module.exports = {
|
||||||
weekDayName: days[date.getDay()],
|
weekDayName: days[date.getDay()],
|
||||||
monthName: months[date.getMonth()],
|
monthName: months[date.getMonth()],
|
||||||
day: date.getDate(),
|
day: date.getDate(),
|
||||||
time: this.formatTime(date),
|
time: this.formatTime(date, showTwelveHour),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this.formatFullDate(date, showTwelveHour);
|
return this.formatFullDate(date, showTwelveHour);
|
||||||
|
@ -92,13 +92,13 @@ module.exports = {
|
||||||
monthName: months[date.getMonth()],
|
monthName: months[date.getMonth()],
|
||||||
day: date.getDate(),
|
day: date.getDate(),
|
||||||
fullYear: date.getFullYear(),
|
fullYear: date.getFullYear(),
|
||||||
time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date),
|
time: this.formatTime(date, showTwelveHour),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTime: function(date, showTwelveHour=false) {
|
formatTime: function(date, showTwelveHour=false) {
|
||||||
if (showTwelveHour) {
|
if (showTwelveHour) {
|
||||||
return twelveHourTime(date);
|
return twelveHourTime(date);
|
||||||
}
|
}
|
||||||
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
},
|
},
|
||||||
|
|
113
src/GroupAddressPicker.js
Normal file
113
src/GroupAddressPicker.js
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
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 Modal from './Modal';
|
||||||
|
import sdk from './';
|
||||||
|
import MultiInviter from './utils/MultiInviter';
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
import GroupStoreCache from './stores/GroupStoreCache';
|
||||||
|
|
||||||
|
export function showGroupInviteDialog(groupId) {
|
||||||
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
|
Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, {
|
||||||
|
title: _t("Invite new group members"),
|
||||||
|
description: _t("Who would you like to add to this group?"),
|
||||||
|
placeholder: _t("Name or matrix ID"),
|
||||||
|
button: _t("Invite to Group"),
|
||||||
|
validAddressTypes: ['mx-user-id'],
|
||||||
|
onFinished: (success, addrs) => {
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
_onGroupInviteFinished(groupId, addrs);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showGroupAddRoomDialog(groupId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
|
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
|
||||||
|
title: _t("Add rooms to the group"),
|
||||||
|
description: _t("Which rooms would you like to add to this group?"),
|
||||||
|
placeholder: _t("Room name or alias"),
|
||||||
|
button: _t("Add to group"),
|
||||||
|
pickerType: 'room',
|
||||||
|
validAddressTypes: ['mx-room-id'],
|
||||||
|
onFinished: (success, addrs) => {
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
_onGroupAddRoomFinished(groupId, addrs).then(resolve, reject);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onGroupInviteFinished(groupId, addrs) {
|
||||||
|
const multiInviter = new MultiInviter(groupId);
|
||||||
|
|
||||||
|
const addrTexts = addrs.map((addr) => addr.address);
|
||||||
|
|
||||||
|
multiInviter.invite(addrTexts).then((completionStates) => {
|
||||||
|
// Show user any errors
|
||||||
|
const errorList = [];
|
||||||
|
for (const addr of Object.keys(completionStates)) {
|
||||||
|
if (addrs[addr] === "error") {
|
||||||
|
errorList.push(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorList.length > 0) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Failed to invite the following users to the group', '', ErrorDialog, {
|
||||||
|
title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}),
|
||||||
|
description: errorList.join(", "),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Failed to invite users to group', '', ErrorDialog, {
|
||||||
|
title: _t("Failed to invite users group"),
|
||||||
|
description: _t("Failed to invite users to %(groupId)s", {groupId: groupId}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onGroupAddRoomFinished(groupId, addrs) {
|
||||||
|
const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
|
||||||
|
const errorList = [];
|
||||||
|
return Promise.all(addrs.map((addr) => {
|
||||||
|
return groupStore
|
||||||
|
.addRoomToGroup(addr.address)
|
||||||
|
.catch(() => { errorList.push(addr.address); })
|
||||||
|
.reflect();
|
||||||
|
})).then(() => {
|
||||||
|
if (errorList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Failed to add the following room to the group',
|
||||||
|
'', ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t(
|
||||||
|
"Failed to add the following rooms to %(groupId)s:",
|
||||||
|
{groupId},
|
||||||
|
),
|
||||||
|
description: errorList.join(", "),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 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.
|
||||||
|
@ -31,13 +32,33 @@ emojione.imagePathPNG = 'emojione/png/';
|
||||||
// Use SVGs for emojis
|
// Use SVGs for emojis
|
||||||
emojione.imageType = 'svg';
|
emojione.imageType = 'svg';
|
||||||
|
|
||||||
|
// Anything outside the basic multilingual plane will be a surrogate pair
|
||||||
|
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
||||||
|
// And there a bunch more symbol characters that emojione has within the
|
||||||
|
// BMP, so this includes the ranges from 'letterlike symbols' to
|
||||||
|
// 'miscellaneous symbols and arrows' which should catch all of them
|
||||||
|
// (with plenty of false positives, but that's OK)
|
||||||
|
const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
|
||||||
|
|
||||||
|
// And this is emojione's complete regex
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return true if the given string contains emoji
|
||||||
|
* Uses a much, much simpler regex than emojione's so will give false
|
||||||
|
* positives, but useful for fast-path testing strings to see if they
|
||||||
|
* need emojification.
|
||||||
|
* unicodeToImage uses this function.
|
||||||
|
*/
|
||||||
|
export function containsEmoji(str) {
|
||||||
|
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
|
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
|
||||||
* because we want to include emoji shortnames in title text
|
* because we want to include emoji shortnames in title text
|
||||||
*/
|
*/
|
||||||
export function unicodeToImage(str) {
|
function unicodeToImage(str) {
|
||||||
let replaceWith, unicode, alt, short, fname;
|
let replaceWith, unicode, alt, short, fname;
|
||||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||||
|
|
||||||
|
@ -127,7 +148,7 @@ export function processHtmlForSending(html: string): string {
|
||||||
* of that HTML.
|
* of that HTML.
|
||||||
*/
|
*/
|
||||||
export function sanitizedHtmlNode(insaneHtml) {
|
export function sanitizedHtmlNode(insaneHtml) {
|
||||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||||
|
|
||||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||||
}
|
}
|
||||||
|
@ -136,7 +157,7 @@ const sanitizeHtmlParams = {
|
||||||
allowedTags: [
|
allowedTags: [
|
||||||
'font', // custom to matrix for IRC-style font coloring
|
'font', // custom to matrix for IRC-style font coloring
|
||||||
'del', // for markdown
|
'del', // for markdown
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub',
|
||||||
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
|
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
|
||||||
],
|
],
|
||||||
|
@ -375,6 +396,8 @@ export function bodyToHtml(content, highlights, opts) {
|
||||||
var isHtml = (content.format === "org.matrix.custom.html");
|
var isHtml = (content.format === "org.matrix.custom.html");
|
||||||
let body = isHtml ? content.formatted_body : escape(content.body);
|
let body = isHtml ? content.formatted_body : escape(content.body);
|
||||||
|
|
||||||
|
let bodyHasEmoji = false;
|
||||||
|
|
||||||
var safeBody;
|
var safeBody;
|
||||||
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
||||||
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
||||||
|
@ -392,17 +415,20 @@ export function bodyToHtml(content, highlights, opts) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
|
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
|
||||||
safeBody = unicodeToImage(safeBody);
|
bodyHasEmoji = containsEmoji(body);
|
||||||
safeBody = addCodeCopyButton(safeBody);
|
if (bodyHasEmoji) safeBody = unicodeToImage(safeBody);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
delete sanitizeHtmlParams.textFilter;
|
delete sanitizeHtmlParams.textFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
EMOJI_REGEX.lastIndex = 0;
|
let emojiBody = false;
|
||||||
let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
if (bodyHasEmoji) {
|
||||||
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
EMOJI_REGEX.lastIndex = 0;
|
||||||
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
||||||
|
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||||
|
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||||
|
}
|
||||||
|
|
||||||
const className = classNames({
|
const className = classNames({
|
||||||
'mx_EventTile_body': true,
|
'mx_EventTile_body': true,
|
||||||
|
@ -412,23 +438,6 @@ export function bodyToHtml(content, highlights, opts) {
|
||||||
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />;
|
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCodeCopyButton(safeBody) {
|
|
||||||
// Adds 'copy' buttons to pre blocks
|
|
||||||
// Note that this only manipulates the markup to add the buttons:
|
|
||||||
// we need to add the event handlers once the nodes are in the DOM
|
|
||||||
// since we can't save functions in the markup.
|
|
||||||
// This is done in TextualBody
|
|
||||||
const el = document.createElement("div");
|
|
||||||
el.innerHTML = safeBody;
|
|
||||||
const codeBlocks = Array.from(el.getElementsByTagName("pre"));
|
|
||||||
codeBlocks.forEach(p => {
|
|
||||||
const button = document.createElement("span");
|
|
||||||
button.className = "mx_EventTile_copyButton";
|
|
||||||
p.appendChild(button);
|
|
||||||
});
|
|
||||||
return el.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function emojifyText(text) {
|
export function emojifyText(text) {
|
||||||
return {
|
return {
|
||||||
__html: unicodeToImage(escape(text)),
|
__html: unicodeToImage(escape(text)),
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import commonmark from 'commonmark';
|
import commonmark from 'commonmark';
|
||||||
import escape from 'lodash/escape';
|
import escape from 'lodash/escape';
|
||||||
|
|
||||||
const ALLOWED_HTML_TAGS = ['del', 'u'];
|
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
||||||
|
|
||||||
// These types of node are definitely text
|
// These types of node are definitely text
|
||||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations 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.
|
||||||
|
@ -33,9 +34,16 @@ import Modal from './Modal';
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const MAX_PENDING_ENCRYPTED = 20;
|
||||||
|
|
||||||
const Notifier = {
|
const Notifier = {
|
||||||
notifsByRoom: {},
|
notifsByRoom: {},
|
||||||
|
|
||||||
|
// A list of event IDs that we've received but need to wait until
|
||||||
|
// they're decrypted until we decide whether to notify for them
|
||||||
|
// or not
|
||||||
|
pendingEncryptedEventIds: [],
|
||||||
|
|
||||||
notificationMessageForEvent: function(ev) {
|
notificationMessageForEvent: function(ev) {
|
||||||
return TextForEvent.textForEvent(ev);
|
return TextForEvent.textForEvent(ev);
|
||||||
},
|
},
|
||||||
|
@ -89,17 +97,18 @@ const Notifier = {
|
||||||
_playAudioNotification: function(ev, room) {
|
_playAudioNotification: function(ev, room) {
|
||||||
const e = document.getElementById("messageAudio");
|
const e = document.getElementById("messageAudio");
|
||||||
if (e) {
|
if (e) {
|
||||||
e.load();
|
|
||||||
e.play();
|
e.play();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
start: function() {
|
start: function() {
|
||||||
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
|
this.boundOnEvent = this.onEvent.bind(this);
|
||||||
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
||||||
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
||||||
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
|
this.boundOnEventDecrypted = this.onEventDecrypted.bind(this);
|
||||||
|
MatrixClientPeg.get().on('event', this.boundOnEvent);
|
||||||
MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
|
MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
|
||||||
|
MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted);
|
||||||
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
||||||
this.toolbarHidden = false;
|
this.toolbarHidden = false;
|
||||||
this.isSyncing = false;
|
this.isSyncing = false;
|
||||||
|
@ -107,8 +116,9 @@ const Notifier = {
|
||||||
|
|
||||||
stop: function() {
|
stop: function() {
|
||||||
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
|
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
|
||||||
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
|
MatrixClientPeg.get().removeListener('Event', this.boundOnEvent);
|
||||||
MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
|
MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
|
||||||
|
MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted);
|
||||||
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
||||||
}
|
}
|
||||||
this.isSyncing = false;
|
this.isSyncing = false;
|
||||||
|
@ -237,23 +247,30 @@ const Notifier = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
onEvent: function(ev) {
|
||||||
if (toStartOfTimeline) return;
|
|
||||||
if (!room) return;
|
|
||||||
if (!this.isSyncing) return; // don't alert for any messages initially
|
if (!this.isSyncing) return; // don't alert for any messages initially
|
||||||
if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
|
if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||||
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
|
||||||
|
|
||||||
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
// If it's an encrypted event and the type is still 'm.room.encrypted',
|
||||||
if (actions && actions.notify) {
|
// it hasn't yet been decrypted, so wait until it is.
|
||||||
if (this.isEnabled()) {
|
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) {
|
||||||
this._displayPopupNotification(ev, room);
|
this.pendingEncryptedEventIds.push(ev.getId());
|
||||||
}
|
// don't let the list fill up indefinitely
|
||||||
if (actions.tweaks.sound && this.isAudioEnabled()) {
|
while (this.pendingEncryptedEventIds.length > MAX_PENDING_ENCRYPTED) {
|
||||||
PlatformPeg.get().loudNotification(ev, room);
|
this.pendingEncryptedEventIds.shift();
|
||||||
this._playAudioNotification(ev, room);
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._evaluateEvent(ev);
|
||||||
|
},
|
||||||
|
|
||||||
|
onEventDecrypted: function(ev) {
|
||||||
|
const idx = this.pendingEncryptedEventIds.indexOf(ev.getId());
|
||||||
|
if (idx === -1) return;
|
||||||
|
|
||||||
|
this.pendingEncryptedEventIds.splice(idx, 1);
|
||||||
|
this._evaluateEvent(ev);
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomReceipt: function(ev, room) {
|
onRoomReceipt: function(ev, room) {
|
||||||
|
@ -273,6 +290,20 @@ const Notifier = {
|
||||||
delete this.notifsByRoom[room.roomId];
|
delete this.notifsByRoom[room.roomId];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_evaluateEvent: function(ev) {
|
||||||
|
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
|
||||||
|
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||||
|
if (actions && actions.notify) {
|
||||||
|
if (this.isEnabled()) {
|
||||||
|
this._displayPopupNotification(ev, room);
|
||||||
|
}
|
||||||
|
if (actions.tweaks.sound && this.isAudioEnabled()) {
|
||||||
|
PlatformPeg.get().loudNotification(ev, room);
|
||||||
|
this._playAudioNotification(ev, room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!global.mxNotifier) {
|
if (!global.mxNotifier) {
|
||||||
|
|
|
@ -28,7 +28,7 @@ export function inviteToRoom(roomId, addr) {
|
||||||
|
|
||||||
if (addrType == 'email') {
|
if (addrType == 'email') {
|
||||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||||
} else if (addrType == 'mx') {
|
} else if (addrType == 'mx-user-id') {
|
||||||
return MatrixClientPeg.get().invite(roomId, addr);
|
return MatrixClientPeg.get().invite(roomId, addr);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unsupported address');
|
throw new Error('Unsupported address');
|
||||||
|
@ -50,8 +50,8 @@ export function inviteMultipleToRoom(roomId, addrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showStartChatInviteDialog() {
|
export function showStartChatInviteDialog() {
|
||||||
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
Modal.createTrackedDialog('Start a chat', '', UserPickerDialog, {
|
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
||||||
title: _t('Start a chat'),
|
title: _t('Start a chat'),
|
||||||
description: _t("Who would you like to communicate with?"),
|
description: _t("Who would you like to communicate with?"),
|
||||||
placeholder: _t("Email, name or matrix ID"),
|
placeholder: _t("Email, name or matrix ID"),
|
||||||
|
@ -61,8 +61,8 @@ export function showStartChatInviteDialog() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showRoomInviteDialog(roomId) {
|
export function showRoomInviteDialog(roomId) {
|
||||||
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
Modal.createTrackedDialog('Chat Invite', '', UserPickerDialog, {
|
Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, {
|
||||||
title: _t('Invite new room members'),
|
title: _t('Invite new room members'),
|
||||||
description: _t('Who would you like to add to this room?'),
|
description: _t('Who would you like to add to this room?'),
|
||||||
button: _t('Send Invites'),
|
button: _t('Send Invites'),
|
||||||
|
@ -127,7 +127,7 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function _isDmChat(addrTexts) {
|
function _isDmChat(addrTexts) {
|
||||||
if (addrTexts.length === 1 && getAddressType(addrTexts[0])) {
|
if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx') {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
|
@ -76,10 +76,13 @@ class ScalarAuthClient {
|
||||||
return defer.promise;
|
return defer.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScalarInterfaceUrlForRoom(roomId, screen) {
|
getScalarInterfaceUrlForRoom(roomId, screen, id) {
|
||||||
var url = SdkConfig.get().integrations_ui_url;
|
var url = SdkConfig.get().integrations_ui_url;
|
||||||
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
||||||
url += "&room_id=" + encodeURIComponent(roomId);
|
url += "&room_id=" + encodeURIComponent(roomId);
|
||||||
|
if (id) {
|
||||||
|
url += '&integ_id=' + encodeURIComponent(id);
|
||||||
|
}
|
||||||
if (screen) {
|
if (screen) {
|
||||||
url += '&screen=' + encodeURIComponent(screen);
|
url += '&screen=' + encodeURIComponent(screen);
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,9 @@ class Skinner {
|
||||||
// behaviour with multiple copies of files etc. is erratic at best.
|
// behaviour with multiple copies of files etc. is erratic at best.
|
||||||
// XXX: We can still end up with the same file twice in the resulting
|
// XXX: We can still end up with the same file twice in the resulting
|
||||||
// JS bundle which is nonideal.
|
// JS bundle which is nonideal.
|
||||||
|
// See https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/
|
||||||
|
// or https://nodejs.org/api/modules.html#modules_module_caching_caveats
|
||||||
|
// ("Modules are cached based on their resolved filename")
|
||||||
if (global.mxSkinner === undefined) {
|
if (global.mxSkinner === undefined) {
|
||||||
global.mxSkinner = new Skinner();
|
global.mxSkinner = new Skinner();
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,6 +240,59 @@ const commands = {
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
ignore: new Command("ignore", "<userId>", function(roomId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
const userId = matches[1];
|
||||||
|
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||||
|
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
||||||
|
return success(
|
||||||
|
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
||||||
|
title: _t("Ignored user"),
|
||||||
|
description: (
|
||||||
|
<div>
|
||||||
|
<p>{ _t("You are now ignoring %(userId)s", {userId: userId}) }</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
}),
|
||||||
|
|
||||||
|
unignore: new Command("unignore", "<userId>", function(roomId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
const userId = matches[1];
|
||||||
|
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||||
|
const index = ignoredUsers.indexOf(userId);
|
||||||
|
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||||
|
return success(
|
||||||
|
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
||||||
|
title: _t("Unignored user"),
|
||||||
|
description: (
|
||||||
|
<div>
|
||||||
|
<p>{ _t("You are no longer ignoring %(userId)s", {userId: userId}) }</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
}),
|
||||||
|
|
||||||
// Define the power level of a user
|
// Define the power level of a user
|
||||||
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
|
@ -292,6 +345,13 @@ const commands = {
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Open developer tools
|
||||||
|
devtools: new Command("devtools", "", function(roomId) {
|
||||||
|
const DevtoolsDialog = sdk.getComponent("dialogs.DevtoolsDialog");
|
||||||
|
Modal.createDialog(DevtoolsDialog, { roomId });
|
||||||
|
return success();
|
||||||
|
}),
|
||||||
|
|
||||||
// Verify a user, device, and pubkey tuple
|
// Verify a user, device, and pubkey tuple
|
||||||
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
|
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
|
|
|
@ -13,56 +13,67 @@ 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 MatrixClientPeg from "./MatrixClientPeg";
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import CallHandler from "./CallHandler";
|
import CallHandler from './CallHandler';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import * as Roles from './Roles';
|
import * as Roles from './Roles';
|
||||||
|
|
||||||
function textForMemberEvent(ev) {
|
function textForMemberEvent(ev) {
|
||||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||||
var senderName = ev.sender ? ev.sender.name : ev.getSender();
|
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||||
var targetName = ev.target ? ev.target.name : ev.getStateKey();
|
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||||
var ConferenceHandler = CallHandler.getConferenceHandler();
|
const prevContent = ev.getPrevContent();
|
||||||
var reason = ev.getContent().reason ? (
|
const content = ev.getContent();
|
||||||
_t('Reason') + ': ' + ev.getContent().reason
|
|
||||||
) : "";
|
const ConferenceHandler = CallHandler.getConferenceHandler();
|
||||||
switch (ev.getContent().membership) {
|
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
||||||
case 'invite':
|
switch (content.membership) {
|
||||||
var threePidContent = ev.getContent().third_party_invite;
|
case 'invite': {
|
||||||
|
const threePidContent = content.third_party_invite;
|
||||||
if (threePidContent) {
|
if (threePidContent) {
|
||||||
if (threePidContent.display_name) {
|
if (threePidContent.display_name) {
|
||||||
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name});
|
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
||||||
|
targetName,
|
||||||
|
displayName: threePidContent.display_name,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return _t('%(targetName)s accepted an invitation.', {targetName: targetName});
|
return _t('%(targetName)s accepted an invitation.', {targetName});
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||||
return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName});
|
return _t('%(senderName)s requested a VoIP conference.', {senderName});
|
||||||
}
|
} else {
|
||||||
else {
|
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
||||||
return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case 'ban':
|
case 'ban':
|
||||||
return _t(
|
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||||
'%(senderName)s banned %(targetName)s.',
|
|
||||||
{senderName: senderName, targetName: targetName}
|
|
||||||
) + ' ' + reason;
|
|
||||||
case 'join':
|
case 'join':
|
||||||
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
|
if (prevContent && prevContent.membership === 'join') {
|
||||||
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
|
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||||
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname});
|
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {
|
||||||
} else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
|
senderName,
|
||||||
return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname});
|
oldDisplayName: prevContent.displayname,
|
||||||
} else if (ev.getPrevContent().displayname && !ev.getContent().displayname) {
|
displayName: content.displayname,
|
||||||
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname});
|
});
|
||||||
} else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) {
|
} else if (!prevContent.displayname && content.displayname) {
|
||||||
return _t('%(senderName)s removed their profile picture.', {senderName: senderName});
|
return _t('%(senderName)s set their display name to %(displayName)s.', {
|
||||||
} else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
|
senderName,
|
||||||
return _t('%(senderName)s changed their profile picture.', {senderName: senderName});
|
displayName: content.displayname,
|
||||||
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
|
});
|
||||||
return _t('%(senderName)s set a profile picture.', {senderName: senderName});
|
} else if (prevContent.displayname && !content.displayname) {
|
||||||
|
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
||||||
|
senderName,
|
||||||
|
oldDisplayName: prevContent.displayname,
|
||||||
|
});
|
||||||
|
} else if (prevContent.avatar_url && !content.avatar_url) {
|
||||||
|
return _t('%(senderName)s removed their profile picture.', {senderName});
|
||||||
|
} else if (prevContent.avatar_url && content.avatar_url &&
|
||||||
|
prevContent.avatar_url !== content.avatar_url) {
|
||||||
|
return _t('%(senderName)s changed their profile picture.', {senderName});
|
||||||
|
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||||
|
return _t('%(senderName)s set a profile picture.', {senderName});
|
||||||
} else {
|
} else {
|
||||||
// suppress null rejoins
|
// suppress null rejoins
|
||||||
return '';
|
return '';
|
||||||
|
@ -71,73 +82,69 @@ function textForMemberEvent(ev) {
|
||||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||||
return _t('VoIP conference started.');
|
return _t('VoIP conference started.');
|
||||||
}
|
} else {
|
||||||
else {
|
return _t('%(targetName)s joined the room.', {targetName});
|
||||||
return _t('%(targetName)s joined the room.', {targetName: targetName});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (ev.getSender() === ev.getStateKey()) {
|
if (ev.getSender() === ev.getStateKey()) {
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||||
return _t('VoIP conference finished.');
|
return _t('VoIP conference finished.');
|
||||||
|
} else if (prevContent.membership === "invite") {
|
||||||
|
return _t('%(targetName)s rejected the invitation.', {targetName});
|
||||||
|
} else {
|
||||||
|
return _t('%(targetName)s left the room.', {targetName});
|
||||||
}
|
}
|
||||||
else if (ev.getPrevContent().membership === "invite") {
|
} else if (prevContent.membership === "ban") {
|
||||||
return _t('%(targetName)s rejected the invitation.', {targetName: targetName});
|
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
||||||
}
|
} else if (prevContent.membership === "join") {
|
||||||
else {
|
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||||
return _t('%(targetName)s left the room.', {targetName: targetName});
|
} else if (prevContent.membership === "invite") {
|
||||||
}
|
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
||||||
}
|
senderName,
|
||||||
else if (ev.getPrevContent().membership === "ban") {
|
targetName,
|
||||||
return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName});
|
}) + ' ' + reason;
|
||||||
}
|
} else {
|
||||||
else if (ev.getPrevContent().membership === "join") {
|
return _t('%(targetName)s left the room.', {targetName});
|
||||||
return _t(
|
|
||||||
'%(senderName)s kicked %(targetName)s.',
|
|
||||||
{senderName: senderName, targetName: targetName}
|
|
||||||
) + ' ' + reason;
|
|
||||||
}
|
|
||||||
else if (ev.getPrevContent().membership === "invite") {
|
|
||||||
return _t(
|
|
||||||
'%(senderName)s withdrew %(targetName)s\'s invitation.',
|
|
||||||
{senderName: senderName, targetName: targetName}
|
|
||||||
) + ' ' + reason;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return _t('%(targetName)s left the room.', {targetName: targetName});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForTopicEvent(ev) {
|
function textForTopicEvent(ev) {
|
||||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic});
|
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
|
||||||
|
senderDisplayName,
|
||||||
|
topic: ev.getContent().topic,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForRoomNameEvent(ev) {
|
function textForRoomNameEvent(ev) {
|
||||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
|
|
||||||
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
||||||
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName});
|
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
||||||
}
|
}
|
||||||
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name});
|
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
||||||
|
senderDisplayName,
|
||||||
|
roomName: ev.getContent().name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMessageEvent(ev) {
|
function textForMessageEvent(ev) {
|
||||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
var message = senderDisplayName + ': ' + ev.getContent().body;
|
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||||
if (ev.getContent().msgtype === "m.emote") {
|
if (ev.getContent().msgtype === "m.emote") {
|
||||||
message = "* " + senderDisplayName + " " + message;
|
message = "* " + senderDisplayName + " " + message;
|
||||||
} else if (ev.getContent().msgtype === "m.image") {
|
} else if (ev.getContent().msgtype === "m.image") {
|
||||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName});
|
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallAnswerEvent(event) {
|
function textForCallAnswerEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||||
return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported;
|
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallHangupEvent(event) {
|
function textForCallHangupEvent(event) {
|
||||||
|
@ -159,48 +166,52 @@ function textForCallHangupEvent(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallInviteEvent(event) {
|
function textForCallInviteEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
// FIXME: Find a better way to determine this from the event?
|
// FIXME: Find a better way to determine this from the event?
|
||||||
var type = "voice";
|
let callType = "voice";
|
||||||
if (event.getContent().offer && event.getContent().offer.sdp &&
|
if (event.getContent().offer && event.getContent().offer.sdp &&
|
||||||
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
|
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
|
||||||
type = "video";
|
callType = "video";
|
||||||
}
|
}
|
||||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
const supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
||||||
return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported;
|
return _t('%(senderName)s placed a %(callType)s call.', {senderName, callType}) + ' ' + supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForThreePidInviteEvent(event) {
|
function textForThreePidInviteEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name});
|
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
||||||
|
senderName,
|
||||||
|
targetDisplayName: event.getContent().display_name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForHistoryVisibilityEvent(event) {
|
function textForHistoryVisibilityEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
var vis = event.getContent().history_visibility;
|
switch (event.getContent().history_visibility) {
|
||||||
// XXX: This i18n just isn't going to work for languages with different sentence structure.
|
case 'invited':
|
||||||
var text = _t('%(senderName)s made future room history visible to', {senderName: senderName}) + ' ';
|
return _t('%(senderName)s made future room history visible to all room members, '
|
||||||
if (vis === "invited") {
|
+ 'from the point they are invited.', {senderName});
|
||||||
text += _t('all room members, from the point they are invited') + '.';
|
case 'joined':
|
||||||
|
return _t('%(senderName)s made future room history visible to all room members, '
|
||||||
|
+ 'from the point they joined.', {senderName});
|
||||||
|
case 'shared':
|
||||||
|
return _t('%(senderName)s made future room history visible to all room members.', {senderName});
|
||||||
|
case 'world_readable':
|
||||||
|
return _t('%(senderName)s made future room history visible to anyone.', {senderName});
|
||||||
|
default:
|
||||||
|
return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
||||||
|
senderName,
|
||||||
|
visibility: event.getContent().history_visibility,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if (vis === "joined") {
|
|
||||||
text += _t('all room members, from the point they joined') + '.';
|
|
||||||
}
|
|
||||||
else if (vis === "shared") {
|
|
||||||
text += _t('all room members') + '.';
|
|
||||||
}
|
|
||||||
else if (vis === "world_readable") {
|
|
||||||
text += _t('anyone') + '.';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
text += ' ' + _t('unknown') + ' (' + vis + ').';
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForEncryptionEvent(event) {
|
function textForEncryptionEvent(event) {
|
||||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {senderName: senderName, algorithm: event.getContent().algorithm});
|
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {
|
||||||
|
senderName,
|
||||||
|
algorithm: event.getContent().algorithm,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently will only display a change if a user's power level is changed
|
// Currently will only display a change if a user's power level is changed
|
||||||
|
@ -211,18 +222,18 @@ function textForPowerEvent(event) {
|
||||||
}
|
}
|
||||||
const userDefault = event.getContent().users_default || 0;
|
const userDefault = event.getContent().users_default || 0;
|
||||||
// Construct set of userIds
|
// Construct set of userIds
|
||||||
let users = [];
|
const users = [];
|
||||||
Object.keys(event.getContent().users).forEach(
|
Object.keys(event.getContent().users).forEach(
|
||||||
(userId) => {
|
(userId) => {
|
||||||
if (users.indexOf(userId) === -1) users.push(userId);
|
if (users.indexOf(userId) === -1) users.push(userId);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
Object.keys(event.getPrevContent().users).forEach(
|
Object.keys(event.getPrevContent().users).forEach(
|
||||||
(userId) => {
|
(userId) => {
|
||||||
if (users.indexOf(userId) === -1) users.push(userId);
|
if (users.indexOf(userId) === -1) users.push(userId);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
let diff = [];
|
const diff = [];
|
||||||
// XXX: This is also surely broken for i18n
|
// XXX: This is also surely broken for i18n
|
||||||
users.forEach((userId) => {
|
users.forEach((userId) => {
|
||||||
// Previous power level
|
// Previous power level
|
||||||
|
@ -231,11 +242,11 @@ function textForPowerEvent(event) {
|
||||||
const to = event.getContent().users[userId];
|
const to = event.getContent().users[userId];
|
||||||
if (to !== from) {
|
if (to !== from) {
|
||||||
diff.push(
|
diff.push(
|
||||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
|
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
|
||||||
toPowerLevel: Roles.textualPowerLevel(to, userDefault)
|
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -244,28 +255,60 @@ function textForPowerEvent(event) {
|
||||||
}
|
}
|
||||||
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
powerLevelDiffText: diff.join(", ")
|
powerLevelDiffText: diff.join(", "),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var handlers = {
|
function textForWidgetEvent(event) {
|
||||||
|
const senderName = event.getSender();
|
||||||
|
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
|
||||||
|
const {name, type, url} = event.getContent() || {};
|
||||||
|
|
||||||
|
let widgetName = name || prevName || type || prevType || '';
|
||||||
|
// Apply sentence case to widget name
|
||||||
|
if (widgetName && widgetName.length > 0) {
|
||||||
|
widgetName = widgetName[0].toUpperCase() + widgetName.slice(1) + ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the widget was removed, its content should be {}, but this is sufficiently
|
||||||
|
// equivalent to that condition.
|
||||||
|
if (url) {
|
||||||
|
if (prevUrl) {
|
||||||
|
return _t('%(widgetName)s widget modified by %(senderName)s', {
|
||||||
|
widgetName, senderName,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return _t('%(widgetName)s widget added by %(senderName)s', {
|
||||||
|
widgetName, senderName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return _t('%(widgetName)s widget removed by %(senderName)s', {
|
||||||
|
widgetName, senderName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
'm.room.message': textForMessageEvent,
|
'm.room.message': textForMessageEvent,
|
||||||
'm.room.name': textForRoomNameEvent,
|
'm.room.name': textForRoomNameEvent,
|
||||||
'm.room.topic': textForTopicEvent,
|
'm.room.topic': textForTopicEvent,
|
||||||
'm.room.member': textForMemberEvent,
|
'm.room.member': textForMemberEvent,
|
||||||
'm.call.invite': textForCallInviteEvent,
|
'm.call.invite': textForCallInviteEvent,
|
||||||
'm.call.answer': textForCallAnswerEvent,
|
'm.call.answer': textForCallAnswerEvent,
|
||||||
'm.call.hangup': textForCallHangupEvent,
|
'm.call.hangup': textForCallHangupEvent,
|
||||||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||||
'm.room.encryption': textForEncryptionEvent,
|
'm.room.encryption': textForEncryptionEvent,
|
||||||
'm.room.power_levels': textForPowerEvent,
|
'm.room.power_levels': textForPowerEvent,
|
||||||
|
|
||||||
|
'im.vector.modular.widgets': textForWidgetEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
textForEvent: function(ev) {
|
textForEvent: function(ev) {
|
||||||
var hdlr = handlers[ev.getType()];
|
const hdlr = handlers[ev.getType()];
|
||||||
if (!hdlr) return "";
|
if (!hdlr) return '';
|
||||||
return hdlr(ev);
|
return hdlr(ev);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,11 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||||
|
|
||||||
const mxidRegex = /^@\S+:\S+$/;
|
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||||
|
const mxRoomIdRegex = /^!\S+:\S+$/;
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
export const addressTypes = [
|
export const addressTypes = [
|
||||||
'mx', 'email',
|
'mx-user-id', 'mx-room-id', 'email',
|
||||||
];
|
];
|
||||||
|
|
||||||
// PropType definition for an object describing
|
// PropType definition for an object describing
|
||||||
|
@ -41,13 +42,16 @@ export const UserAddressType = PropTypes.shape({
|
||||||
|
|
||||||
export function getAddressType(inputText) {
|
export function getAddressType(inputText) {
|
||||||
const isEmailAddress = emailRegex.test(inputText);
|
const isEmailAddress = emailRegex.test(inputText);
|
||||||
const isMatrixId = mxidRegex.test(inputText);
|
const isUserId = mxUserIdRegex.test(inputText);
|
||||||
|
const isRoomId = mxRoomIdRegex.test(inputText);
|
||||||
|
|
||||||
// sanity check the input for user IDs
|
// sanity check the input for user IDs
|
||||||
if (isEmailAddress) {
|
if (isEmailAddress) {
|
||||||
return 'email';
|
return 'email';
|
||||||
} else if (isMatrixId) {
|
} else if (isUserId) {
|
||||||
return 'mx';
|
return 'mx-user-id';
|
||||||
|
} else if (isRoomId) {
|
||||||
|
return 'mx-room-id';
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,17 @@ export default {
|
||||||
// XXX: Always use default, ignore localStorage and remove from labs
|
// XXX: Always use default, ignore localStorage and remove from labs
|
||||||
override: true,
|
override: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "-",
|
||||||
|
id: 'feature_groups',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// horrible but it works. The locality makes this somewhat more palatable.
|
// horrible but it works. The locality makes this somewhat more palatable.
|
||||||
doTranslations: function() {
|
doTranslations: function() {
|
||||||
this.LABS_FEATURES[0].name = _t("Matrix Apps");
|
this.LABS_FEATURES[0].name = _t("Matrix Apps");
|
||||||
|
this.LABS_FEATURES[1].name = _t("Groups");
|
||||||
},
|
},
|
||||||
|
|
||||||
loadProfileInfo: function() {
|
loadProfileInfo: function() {
|
||||||
|
|
|
@ -18,6 +18,12 @@ var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
usersTypingApartFromMeAndIgnored: function(room) {
|
||||||
|
return this.usersTyping(
|
||||||
|
room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
usersTypingApartFromMe: function(room) {
|
usersTypingApartFromMe: function(room) {
|
||||||
return this.usersTyping(
|
return this.usersTyping(
|
||||||
room, [MatrixClientPeg.get().credentials.userId]
|
room, [MatrixClientPeg.get().credentials.userId]
|
||||||
|
|
|
@ -136,13 +136,13 @@ export default React.createClass({
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<div className='error'>
|
<div className='error'>
|
||||||
{this.state.errStr}
|
{ this.state.errStr }
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputTable'>
|
<div className='mx_E2eKeysDialog_inputTable'>
|
||||||
<div className='mx_E2eKeysDialog_inputRow'>
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
<label htmlFor='passphrase1'>
|
<label htmlFor='passphrase1'>
|
||||||
{_t("Enter passphrase")}
|
{ _t("Enter passphrase") }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
@ -155,7 +155,7 @@ export default React.createClass({
|
||||||
<div className='mx_E2eKeysDialog_inputRow'>
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
<label htmlFor='passphrase2'>
|
<label htmlFor='passphrase2'>
|
||||||
{_t("Confirm passphrase")}
|
{ _t("Confirm passphrase") }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
@ -172,7 +172,7 @@ export default React.createClass({
|
||||||
disabled={disableForm}
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
{_t("Cancel")}
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -134,13 +134,13 @@ export default React.createClass({
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<div className='error'>
|
<div className='error'>
|
||||||
{this.state.errStr}
|
{ this.state.errStr }
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputTable'>
|
<div className='mx_E2eKeysDialog_inputTable'>
|
||||||
<div className='mx_E2eKeysDialog_inputRow'>
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
<label htmlFor='importFile'>
|
<label htmlFor='importFile'>
|
||||||
{_t("File to import")}
|
{ _t("File to import") }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
@ -153,14 +153,14 @@ export default React.createClass({
|
||||||
<div className='mx_E2eKeysDialog_inputRow'>
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
<label htmlFor='passphrase'>
|
<label htmlFor='passphrase'>
|
||||||
{_t("Enter passphrase")}
|
{ _t("Enter passphrase") }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
<input ref='passphrase' id='passphrase'
|
<input ref='passphrase' id='passphrase'
|
||||||
size='64' type='password'
|
size='64' type='password'
|
||||||
onChange={this._onFormChange}
|
onChange={this._onFormChange}
|
||||||
disabled={disableForm}/>
|
disabled={disableForm} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -170,7 +170,7 @@ export default React.createClass({
|
||||||
disabled={!this.state.enableSubmit || disableForm}
|
disabled={!this.state.enableSubmit || disableForm}
|
||||||
/>
|
/>
|
||||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
{_t("Cancel")}
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t, _td } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import FuzzyMatcher from './FuzzyMatcher';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
|
@ -27,72 +27,82 @@ const COMMANDS = [
|
||||||
{
|
{
|
||||||
command: '/me',
|
command: '/me',
|
||||||
args: '<message>',
|
args: '<message>',
|
||||||
description: 'Displays action',
|
description: _td('Displays action'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/ban',
|
command: '/ban',
|
||||||
args: '<user-id> [reason]',
|
args: '<user-id> [reason]',
|
||||||
description: 'Bans user with given id',
|
description: _td('Bans user with given id'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/unban',
|
command: '/unban',
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
description: 'Unbans user with given id',
|
description: _td('Unbans user with given id'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/op',
|
command: '/op',
|
||||||
args: '<user-id> [<power-level>]',
|
args: '<user-id> [<power-level>]',
|
||||||
description: 'Define the power level of a user',
|
description: _td('Define the power level of a user'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/deop',
|
command: '/deop',
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
description: 'Deops user with given id',
|
description: _td('Deops user with given id'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/invite',
|
command: '/invite',
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
description: 'Invites user with given id to current room',
|
description: _td('Invites user with given id to current room'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/join',
|
command: '/join',
|
||||||
args: '<room-alias>',
|
args: '<room-alias>',
|
||||||
description: 'Joins room with given alias',
|
description: _td('Joins room with given alias'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/part',
|
command: '/part',
|
||||||
args: '[<room-alias>]',
|
args: '[<room-alias>]',
|
||||||
description: 'Leave room',
|
description: _td('Leave room'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/topic',
|
command: '/topic',
|
||||||
args: '<topic>',
|
args: '<topic>',
|
||||||
description: 'Sets the room topic',
|
description: _td('Sets the room topic'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/kick',
|
command: '/kick',
|
||||||
args: '<user-id> [reason]',
|
args: '<user-id> [reason]',
|
||||||
description: 'Kicks user with given id',
|
description: _td('Kicks user with given id'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/nick',
|
command: '/nick',
|
||||||
args: '<display-name>',
|
args: '<display-name>',
|
||||||
description: 'Changes your display nickname',
|
description: _td('Changes your display nickname'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/ddg',
|
command: '/ddg',
|
||||||
args: '<query>',
|
args: '<query>',
|
||||||
description: 'Searches DuckDuckGo for results',
|
description: _td('Searches DuckDuckGo for results'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/tint',
|
command: '/tint',
|
||||||
args: '<color1> [<color2>]',
|
args: '<color1> [<color2>]',
|
||||||
description: 'Changes colour scheme of current room',
|
description: _td('Changes colour scheme of current room'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/verify',
|
command: '/verify',
|
||||||
args: '<user-id> <device-id> <device-signing-key>',
|
args: '<user-id> <device-id> <device-signing-key>',
|
||||||
description: 'Verifies a user, device, and pubkey tuple',
|
description: _td('Verifies a user, device, and pubkey tuple'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/ignore',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Ignores a user, hiding their messages from you'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/unignore',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Stops ignoring a user, showing their messages going forward'),
|
||||||
},
|
},
|
||||||
// Omitting `/markdown` as it only seems to apply to OldComposer
|
// Omitting `/markdown` as it only seems to apply to OldComposer
|
||||||
];
|
];
|
||||||
|
@ -119,7 +129,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
component: (<TextualCompletion
|
component: (<TextualCompletion
|
||||||
title={result.command}
|
title={result.command}
|
||||||
subtitle={result.args}
|
subtitle={result.args}
|
||||||
description={ _t(result.description) }
|
description={_t(result.description)}
|
||||||
/>),
|
/>),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
|
@ -140,7 +150,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_block">
|
return <div className="mx_Autocomplete_Completion_container_block">
|
||||||
{completions}
|
{ completions }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,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 EmojiData from '../stripped-emoji.json';
|
import EmojiData from '../stripped-emoji.json';
|
||||||
|
|
||||||
|
@ -96,6 +97,10 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange) {
|
async getCompletions(query: string, selection: SelectionRange) {
|
||||||
|
if (UserSettingsStore.getSyncedSetting("MessageComposerInput.dontSuggestEmoji")) {
|
||||||
|
return []; // don't give any suggestions if the user doesn't want them
|
||||||
|
}
|
||||||
|
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
|
|
|
@ -106,7 +106,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||||
{completions}
|
{ completions }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,8 @@ const USER_REGEX = /@\S*/g;
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
export default class UserProvider extends AutocompleteProvider {
|
export default class UserProvider extends AutocompleteProvider {
|
||||||
users: Array<RoomMember> = [];
|
users: Array<RoomMember> = null;
|
||||||
|
room: Room = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(USER_REGEX, {
|
super(USER_REGEX, {
|
||||||
|
@ -54,6 +55,9 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lazy-load user list into matcher
|
||||||
|
if (this.users === null) this._makeUsers();
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
|
@ -83,7 +87,12 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserListFromRoom(room: Room) {
|
setUserListFromRoom(room: Room) {
|
||||||
const events = room.getLiveTimeline().getEvents();
|
this.room = room;
|
||||||
|
this.users = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeUsers() {
|
||||||
|
const events = this.room.getLiveTimeline().getEvents();
|
||||||
const lastSpoken = {};
|
const lastSpoken = {};
|
||||||
|
|
||||||
for(const event of events) {
|
for(const event of events) {
|
||||||
|
@ -91,7 +100,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
this.users = room.getJoinedMembers().filter((member) => {
|
this.users = this.room.getJoinedMembers().filter((member) => {
|
||||||
if (member.userId !== currentUserId) return true;
|
if (member.userId !== currentUserId) return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -103,7 +112,8 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserSpoke(user: RoomMember) {
|
onUserSpoke(user: RoomMember) {
|
||||||
if(user.userId === MatrixClientPeg.get().credentials.userId) return;
|
if (this.users === null) return;
|
||||||
|
if (user.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||||
|
|
||||||
// Move the user that spoke to the front of the array
|
// Move the user that spoke to the front of the array
|
||||||
this.users.splice(
|
this.users.splice(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd.
|
Copyright 2017 Vector Creations 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.
|
||||||
|
@ -16,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Promise from 'bluebird';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
|
@ -25,6 +27,9 @@ import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import GroupStoreCache from '../../stores/GroupStoreCache';
|
||||||
|
import GroupStore from '../../stores/GroupStore';
|
||||||
|
|
||||||
const RoomSummaryType = PropTypes.shape({
|
const RoomSummaryType = PropTypes.shape({
|
||||||
room_id: PropTypes.string.isRequired,
|
room_id: PropTypes.string.isRequired,
|
||||||
profile: PropTypes.shape({
|
profile: PropTypes.shape({
|
||||||
|
@ -37,6 +42,9 @@ const RoomSummaryType = PropTypes.shape({
|
||||||
const UserSummaryType = PropTypes.shape({
|
const UserSummaryType = PropTypes.shape({
|
||||||
summaryInfo: PropTypes.shape({
|
summaryInfo: PropTypes.shape({
|
||||||
user_id: PropTypes.string.isRequired,
|
user_id: PropTypes.string.isRequired,
|
||||||
|
role_id: PropTypes.string,
|
||||||
|
avatar_url: PropTypes.string,
|
||||||
|
displayname: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -50,19 +58,79 @@ const CategoryRoomList = React.createClass({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
}),
|
}),
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// Whether the list should be editable
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
onAddRoomsClicked: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
|
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
|
||||||
|
title: _t('Add rooms to the group summary'),
|
||||||
|
description: _t("Which rooms would you like to add to this summary?"),
|
||||||
|
placeholder: _t("Room name or alias"),
|
||||||
|
button: _t("Add to summary"),
|
||||||
|
pickerType: 'room',
|
||||||
|
validAddressTypes: ['mx-room-id'],
|
||||||
|
groupId: this.props.groupId,
|
||||||
|
onFinished: (success, addrs) => {
|
||||||
|
if (!success) return;
|
||||||
|
const errorList = [];
|
||||||
|
Promise.all(addrs.map((addr) => {
|
||||||
|
return this.context.groupStore
|
||||||
|
.addRoomToGroupSummary(addr.address)
|
||||||
|
.catch(() => { errorList.push(addr.address); })
|
||||||
|
.reflect();
|
||||||
|
})).then(() => {
|
||||||
|
if (errorList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Failed to add the following room to the group summary',
|
||||||
|
'', ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t(
|
||||||
|
"Failed to add the following rooms to the summary of %(groupId)s:",
|
||||||
|
{groupId: this.props.groupId},
|
||||||
|
),
|
||||||
|
description: errorList.join(", "),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
const addButton = this.props.editing ?
|
||||||
|
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddRoomsClicked}>
|
||||||
|
<TintableSvg src="img/icons-create-room.svg" width="64" height="64" />
|
||||||
|
<div className="mx_GroupView_featuredThings_addButton_label">
|
||||||
|
{ _t('Add a Room') }
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>) : <div />;
|
||||||
|
|
||||||
const roomNodes = this.props.rooms.map((r) => {
|
const roomNodes = this.props.rooms.map((r) => {
|
||||||
return <FeaturedRoom key={r.room_id} summaryInfo={r} />;
|
return <FeaturedRoom
|
||||||
|
key={r.room_id}
|
||||||
|
groupId={this.props.groupId}
|
||||||
|
editing={this.props.editing}
|
||||||
|
summaryInfo={r} />;
|
||||||
});
|
});
|
||||||
let catHeader = null;
|
|
||||||
|
let catHeader = <div />;
|
||||||
if (this.props.category && this.props.category.profile) {
|
if (this.props.category && this.props.category.profile) {
|
||||||
catHeader = <div className="mx_GroupView_featuredThings_category">{this.props.category.profile.name}</div>;
|
catHeader = <div className="mx_GroupView_featuredThings_category">
|
||||||
|
{ this.props.category.profile.name }
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
return <div>
|
return <div className="mx_GroupView_featuredThings_container">
|
||||||
{catHeader}
|
{ catHeader }
|
||||||
{roomNodes}
|
{ roomNodes }
|
||||||
|
{ addButton }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -72,6 +140,8 @@ const FeaturedRoom = React.createClass({
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
summaryInfo: RoomSummaryType.isRequired,
|
summaryInfo: RoomSummaryType.isRequired,
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function(e) {
|
onClick: function(e) {
|
||||||
|
@ -85,28 +155,69 @@ const FeaturedRoom = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onDeleteClicked: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.context.groupStore.removeRoomFromGroupSummary(
|
||||||
|
this.props.summaryInfo.room_id,
|
||||||
|
).catch((err) => {
|
||||||
|
console.error('Error whilst removing room from group summary', err);
|
||||||
|
const roomName = this.props.summaryInfo.name ||
|
||||||
|
this.props.summaryInfo.canonical_alias ||
|
||||||
|
this.props.summaryInfo.room_id;
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Failed to remove room from group summary',
|
||||||
|
'', ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t(
|
||||||
|
"Failed to remove the room from the summary of %(groupId)s",
|
||||||
|
{groupId: this.props.groupId},
|
||||||
|
),
|
||||||
|
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||||
|
|
||||||
|
const roomName = this.props.summaryInfo.profile.name ||
|
||||||
|
this.props.summaryInfo.profile.canonical_alias ||
|
||||||
|
_t("Unnamed Room");
|
||||||
|
|
||||||
const oobData = {
|
const oobData = {
|
||||||
roomId: this.props.summaryInfo.room_id,
|
roomId: this.props.summaryInfo.room_id,
|
||||||
avatarUrl: this.props.summaryInfo.profile.avatar_url,
|
avatarUrl: this.props.summaryInfo.profile.avatar_url,
|
||||||
name: this.props.summaryInfo.profile.name,
|
name: roomName,
|
||||||
};
|
};
|
||||||
|
|
||||||
let permalink = null;
|
let permalink = null;
|
||||||
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
|
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
|
||||||
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
|
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomNameNode = null;
|
let roomNameNode = null;
|
||||||
if (permalink) {
|
if (permalink) {
|
||||||
roomNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.profile.name}</a>;
|
roomNameNode = <a href={permalink} onClick={this.onClick} >{ roomName }</a>;
|
||||||
} else {
|
} else {
|
||||||
roomNameNode = <span>{this.props.summaryInfo.profile.name}</span>;
|
roomNameNode = <span>{ roomName }</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteButton = this.props.editing ?
|
||||||
|
<img
|
||||||
|
className="mx_GroupView_featuredThing_deleteButton"
|
||||||
|
src="img/cancel-small.svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
alt="Delete"
|
||||||
|
onClick={this.onDeleteClicked} />
|
||||||
|
: <div />;
|
||||||
|
|
||||||
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
||||||
<RoomAvatar oobData={oobData} width={64} height={64} />
|
<RoomAvatar oobData={oobData} width={64} height={64} />
|
||||||
<div className="mx_GroupView_featuredThing_name">{roomNameNode}</div>
|
<div className="mx_GroupView_featuredThing_name">{ roomNameNode }</div>
|
||||||
|
{ deleteButton }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -121,19 +232,75 @@ const RoleUserList = React.createClass({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
}),
|
}),
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// Whether the list should be editable
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
onAddUsersClicked: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
|
Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, {
|
||||||
|
title: _t('Add users to the group summary'),
|
||||||
|
description: _t("Who would you like to add to this summary?"),
|
||||||
|
placeholder: _t("Name or matrix ID"),
|
||||||
|
button: _t("Add to summary"),
|
||||||
|
validAddressTypes: ['mx-user-id'],
|
||||||
|
groupId: this.props.groupId,
|
||||||
|
shouldOmitSelf: false,
|
||||||
|
onFinished: (success, addrs) => {
|
||||||
|
if (!success) return;
|
||||||
|
const errorList = [];
|
||||||
|
Promise.all(addrs.map((addr) => {
|
||||||
|
return this.context.groupStore
|
||||||
|
.addUserToGroupSummary(addr.address)
|
||||||
|
.catch(() => { errorList.push(addr.address); })
|
||||||
|
.reflect();
|
||||||
|
})).then(() => {
|
||||||
|
if (errorList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Failed to add the following users to the group summary',
|
||||||
|
'', ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t(
|
||||||
|
"Failed to add the following users to the summary of %(groupId)s:",
|
||||||
|
{groupId: this.props.groupId},
|
||||||
|
),
|
||||||
|
description: errorList.join(", "),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
const addButton = this.props.editing ?
|
||||||
|
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}>
|
||||||
|
<TintableSvg src="img/icons-create-room.svg" width="64" height="64" />
|
||||||
|
<div className="mx_GroupView_featuredThings_addButton_label">
|
||||||
|
{ _t('Add a User') }
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>) : <div />;
|
||||||
const userNodes = this.props.users.map((u) => {
|
const userNodes = this.props.users.map((u) => {
|
||||||
return <FeaturedUser key={u.user_id} summaryInfo={u} />;
|
return <FeaturedUser
|
||||||
|
key={u.user_id}
|
||||||
|
summaryInfo={u}
|
||||||
|
editing={this.props.editing}
|
||||||
|
groupId={this.props.groupId} />;
|
||||||
});
|
});
|
||||||
let roleHeader = null;
|
let roleHeader = <div />;
|
||||||
if (this.props.role && this.props.role.profile) {
|
if (this.props.role && this.props.role.profile) {
|
||||||
roleHeader = <div className="mx_GroupView_featuredThings_category">{this.props.role.profile.name}</div>;
|
roleHeader = <div className="mx_GroupView_featuredThings_category">{ this.props.role.profile.name }</div>;
|
||||||
}
|
}
|
||||||
return <div>
|
return <div className="mx_GroupView_featuredThings_container">
|
||||||
{roleHeader}
|
{ roleHeader }
|
||||||
{userNodes}
|
{ userNodes }
|
||||||
|
{ addButton }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -143,6 +310,8 @@ const FeaturedUser = React.createClass({
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
summaryInfo: UserSummaryType.isRequired,
|
summaryInfo: UserSummaryType.isRequired,
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function(e) {
|
onClick: function(e) {
|
||||||
|
@ -156,19 +325,64 @@ const FeaturedUser = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onDeleteClicked: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.context.groupStore.removeUserFromGroupSummary(
|
||||||
|
this.props.summaryInfo.user_id,
|
||||||
|
).catch((err) => {
|
||||||
|
console.error('Error whilst removing user from group summary', err);
|
||||||
|
const displayName = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Failed to remove user from group summary',
|
||||||
|
'', ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t(
|
||||||
|
"Failed to remove a user from the summary of %(groupId)s",
|
||||||
|
{groupId: this.props.groupId},
|
||||||
|
),
|
||||||
|
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
// Add avatar once we get profile info inline in the summary response
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
//const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
|
||||||
|
|
||||||
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
|
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
|
||||||
const userNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.user_id}</a>;
|
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
|
||||||
|
const httpUrl = MatrixClientPeg.get()
|
||||||
|
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
|
||||||
|
|
||||||
|
const deleteButton = this.props.editing ?
|
||||||
|
<img
|
||||||
|
className="mx_GroupView_featuredThing_deleteButton"
|
||||||
|
src="img/cancel-small.svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
alt="Delete"
|
||||||
|
onClick={this.onDeleteClicked} />
|
||||||
|
: <div />;
|
||||||
|
|
||||||
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
||||||
<div className="mx_GroupView_featuredThing_name">{userNameNode}</div>
|
<BaseAvatar name={name} url={httpUrl} width={64} height={64} />
|
||||||
|
<div className="mx_GroupView_featuredThing_name">{ userNameNode }</div>
|
||||||
|
{ deleteButton }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const GroupContext = {
|
||||||
|
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
CategoryRoomList.contextTypes = GroupContext;
|
||||||
|
FeaturedRoom.contextTypes = GroupContext;
|
||||||
|
RoleUserList.contextTypes = GroupContext;
|
||||||
|
FeaturedUser.contextTypes = GroupContext;
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'GroupView',
|
displayName: 'GroupView',
|
||||||
|
|
||||||
|
@ -176,6 +390,16 @@ export default React.createClass({
|
||||||
groupId: PropTypes.string.isRequired,
|
groupId: PropTypes.string.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
childContextTypes: {
|
||||||
|
groupStore: React.PropTypes.instanceOf(GroupStore),
|
||||||
|
},
|
||||||
|
|
||||||
|
getChildContext: function() {
|
||||||
|
return {
|
||||||
|
groupStore: this._groupStore,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
summary: null,
|
summary: null,
|
||||||
|
@ -183,12 +407,21 @@ export default React.createClass({
|
||||||
editing: false,
|
editing: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
uploadingAvatar: false,
|
uploadingAvatar: false,
|
||||||
|
membershipBusy: false,
|
||||||
|
publicityBusy: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._changeAvatarComponent = null;
|
this._changeAvatarComponent = null;
|
||||||
this._loadGroupFromServer(this.props.groupId);
|
this._initGroupStore(this.props.groupId);
|
||||||
|
|
||||||
|
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
this._groupStore.removeAllListeners();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
@ -197,18 +430,26 @@ export default React.createClass({
|
||||||
summary: null,
|
summary: null,
|
||||||
error: null,
|
error: null,
|
||||||
}, () => {
|
}, () => {
|
||||||
this._loadGroupFromServer(newProps.groupId);
|
this._initGroupStore(newProps.groupId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_loadGroupFromServer: function(groupId) {
|
_onGroupMyMembership: function(group) {
|
||||||
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
|
if (group.groupId !== this.props.groupId) return;
|
||||||
|
|
||||||
|
this.setState({membershipBusy: false});
|
||||||
|
},
|
||||||
|
|
||||||
|
_initGroupStore: function(groupId) {
|
||||||
|
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
|
||||||
|
this._groupStore.on('update', () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
summary: res,
|
summary: this._groupStore.getSummary(),
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
});
|
||||||
|
this._groupStore.on('error', (err) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
summary: null,
|
summary: null,
|
||||||
error: err,
|
error: err,
|
||||||
|
@ -216,6 +457,10 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onShowRhsClick: function(ev) {
|
||||||
|
dis.dispatch({ action: 'show_right_panel' });
|
||||||
|
},
|
||||||
|
|
||||||
_onEditClick: function() {
|
_onEditClick: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
editing: true,
|
editing: true,
|
||||||
|
@ -281,7 +526,7 @@ export default React.createClass({
|
||||||
editing: false,
|
editing: false,
|
||||||
summary: null,
|
summary: null,
|
||||||
});
|
});
|
||||||
this._loadGroupFromServer(this.props.groupId);
|
this._initGroupStore(this.props.groupId);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
saving: false,
|
saving: false,
|
||||||
|
@ -295,10 +540,80 @@ export default React.createClass({
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
_getFeaturedRoomsNode() {
|
_onAcceptInviteClick: function() {
|
||||||
const summary = this.state.summary;
|
this.setState({membershipBusy: true});
|
||||||
|
MatrixClientPeg.get().acceptGroupInvite(this.props.groupId).then(() => {
|
||||||
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({membershipBusy: false});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Error accepting invite', '', ErrorDialog, {
|
||||||
|
title: _t("Error"),
|
||||||
|
description: _t("Unable to accept invite"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
if (summary.rooms_section.rooms.length == 0) return null;
|
_onRejectInviteClick: function() {
|
||||||
|
this.setState({membershipBusy: true});
|
||||||
|
MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => {
|
||||||
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({membershipBusy: false});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
|
||||||
|
title: _t("Error"),
|
||||||
|
description: _t("Unable to reject invite"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onLeaveClick: function() {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
|
||||||
|
title: _t("Leave Group"),
|
||||||
|
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
|
||||||
|
button: _t("Leave"),
|
||||||
|
danger: true,
|
||||||
|
onFinished: (confirmed) => {
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
this.setState({membershipBusy: true});
|
||||||
|
MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => {
|
||||||
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({membershipBusy: false});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Error leaving room', '', ErrorDialog, {
|
||||||
|
title: _t("Error"),
|
||||||
|
description: _t("Unable to leave room"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onPubliciseOffClick: function() {
|
||||||
|
this._setPublicity(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onPubliciseOnClick: function() {
|
||||||
|
this._setPublicity(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
_setPublicity: function(publicity) {
|
||||||
|
this.setState({
|
||||||
|
publicityBusy: true,
|
||||||
|
});
|
||||||
|
this._groupStore.setGroupPublicity(publicity).then(() => {
|
||||||
|
this.setState({
|
||||||
|
publicityBusy: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_getFeaturedRoomsNode: function() {
|
||||||
|
const summary = this.state.summary;
|
||||||
|
|
||||||
const defaultCategoryRooms = [];
|
const defaultCategoryRooms = [];
|
||||||
const categoryRooms = {};
|
const categoryRooms = {};
|
||||||
|
@ -315,29 +630,32 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let defaultCategoryNode = null;
|
const defaultCategoryNode = <CategoryRoomList
|
||||||
if (defaultCategoryRooms.length > 0) {
|
rooms={defaultCategoryRooms}
|
||||||
defaultCategoryNode = <CategoryRoomList rooms={defaultCategoryRooms} />;
|
groupId={this.props.groupId}
|
||||||
}
|
editing={this.state.editing} />;
|
||||||
const categoryRoomNodes = Object.keys(categoryRooms).map((catId) => {
|
const categoryRoomNodes = Object.keys(categoryRooms).map((catId) => {
|
||||||
const cat = summary.rooms_section.categories[catId];
|
const cat = summary.rooms_section.categories[catId];
|
||||||
return <CategoryRoomList key={catId} rooms={categoryRooms[catId]} category={cat} />;
|
return <CategoryRoomList
|
||||||
|
key={catId}
|
||||||
|
rooms={categoryRooms[catId]}
|
||||||
|
category={cat}
|
||||||
|
groupId={this.props.groupId}
|
||||||
|
editing={this.state.editing} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className="mx_GroupView_featuredThings">
|
return <div className="mx_GroupView_featuredThings">
|
||||||
<div className="mx_GroupView_featuredThings_header">
|
<div className="mx_GroupView_featuredThings_header">
|
||||||
{_t('Featured Rooms:')}
|
{ _t('Featured Rooms:') }
|
||||||
</div>
|
</div>
|
||||||
{defaultCategoryNode}
|
{ defaultCategoryNode }
|
||||||
{categoryRoomNodes}
|
{ categoryRoomNodes }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getFeaturedUsersNode() {
|
_getFeaturedUsersNode: function() {
|
||||||
const summary = this.state.summary;
|
const summary = this.state.summary;
|
||||||
|
|
||||||
if (summary.users_section.users.length == 0) return null;
|
|
||||||
|
|
||||||
const noRoleUsers = [];
|
const noRoleUsers = [];
|
||||||
const roleUsers = {};
|
const roleUsers = {};
|
||||||
summary.users_section.users.forEach((u) => {
|
summary.users_section.users.forEach((u) => {
|
||||||
|
@ -353,24 +671,121 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let noRoleNode = null;
|
const noRoleNode = <RoleUserList
|
||||||
if (noRoleUsers.length > 0) {
|
users={noRoleUsers}
|
||||||
noRoleNode = <RoleUserList users={noRoleUsers} />;
|
groupId={this.props.groupId}
|
||||||
}
|
editing={this.state.editing} />;
|
||||||
const roleUserNodes = Object.keys(roleUsers).map((roleId) => {
|
const roleUserNodes = Object.keys(roleUsers).map((roleId) => {
|
||||||
const role = summary.users_section.roles[roleId];
|
const role = summary.users_section.roles[roleId];
|
||||||
return <RoleUserList key={roleId} users={roleUsers[roleId]} role={role} />;
|
return <RoleUserList
|
||||||
|
key={roleId}
|
||||||
|
users={roleUsers[roleId]}
|
||||||
|
role={role}
|
||||||
|
groupId={this.props.groupId}
|
||||||
|
editing={this.state.editing} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className="mx_GroupView_featuredThings">
|
return <div className="mx_GroupView_featuredThings">
|
||||||
<div className="mx_GroupView_featuredThings_header">
|
<div className="mx_GroupView_featuredThings_header">
|
||||||
{_t('Featured Users:')}
|
{ _t('Featured Users:') }
|
||||||
</div>
|
</div>
|
||||||
{noRoleNode}
|
{ noRoleNode }
|
||||||
{roleUserNodes}
|
{ roleUserNodes }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_getMembershipSection: function() {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
|
const group = MatrixClientPeg.get().getGroup(this.props.groupId);
|
||||||
|
if (!group) return null;
|
||||||
|
|
||||||
|
if (group.myMembership === 'invite') {
|
||||||
|
if (this.state.membershipBusy) {
|
||||||
|
return <div className="mx_GroupView_membershipSection">
|
||||||
|
<Spinner />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited">
|
||||||
|
<div className="mx_GroupView_membershipSection_description">
|
||||||
|
{ _t("%(inviter)s has invited you to join this group", {inviter: group.inviter.userId}) }
|
||||||
|
</div>
|
||||||
|
<div className="mx_GroupView_membership_buttonContainer">
|
||||||
|
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
|
onClick={this._onAcceptInviteClick}
|
||||||
|
>
|
||||||
|
{ _t("Accept") }
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
|
onClick={this._onRejectInviteClick}
|
||||||
|
>
|
||||||
|
{ _t("Decline") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
} else if (group.myMembership === 'join') {
|
||||||
|
let youAreAMemberText = _t("You are a member of this group");
|
||||||
|
if (this.state.summary.user && this.state.summary.user.is_privileged) {
|
||||||
|
youAreAMemberText = _t("You are an administrator of this group");
|
||||||
|
}
|
||||||
|
|
||||||
|
let publicisedButton;
|
||||||
|
if (this.state.publicityBusy) {
|
||||||
|
publicisedButton = <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let publicisedSection;
|
||||||
|
if (this.state.summary.user && this.state.summary.user.is_publicised) {
|
||||||
|
if (!this.state.publicityBusy) {
|
||||||
|
publicisedButton = <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
|
onClick={this._onPubliciseOffClick}
|
||||||
|
>
|
||||||
|
{ _t("Unpublish") }
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
publicisedSection = <div className="mx_GroupView_membershipSubSection">
|
||||||
|
{ _t("This group is published on your profile") }
|
||||||
|
<div className="mx_GroupView_membership_buttonContainer">
|
||||||
|
{ publicisedButton }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
if (!this.state.publicityBusy) {
|
||||||
|
publicisedButton = <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
|
onClick={this._onPubliciseOnClick}
|
||||||
|
>
|
||||||
|
{ _t("Publish") }
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
publicisedSection = <div className="mx_GroupView_membershipSubSection">
|
||||||
|
{ _t("This group is not published on your profile") }
|
||||||
|
<div className="mx_GroupView_membership_buttonContainer">
|
||||||
|
{ publicisedButton }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_joined">
|
||||||
|
<div className="mx_GroupView_membershipSubSection">
|
||||||
|
<div className="mx_GroupView_membershipSection_description">
|
||||||
|
{ youAreAMemberText }
|
||||||
|
</div>
|
||||||
|
<div className="mx_GroupView_membership_buttonContainer">
|
||||||
|
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
|
onClick={this._onLeaveClick}
|
||||||
|
>
|
||||||
|
{ _t("Leave") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ publicisedSection }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
@ -384,8 +799,8 @@ export default React.createClass({
|
||||||
let avatarNode;
|
let avatarNode;
|
||||||
let nameNode;
|
let nameNode;
|
||||||
let shortDescNode;
|
let shortDescNode;
|
||||||
let rightButtons;
|
|
||||||
let roomBody;
|
let roomBody;
|
||||||
|
const rightButtons = [];
|
||||||
const headerClasses = {
|
const headerClasses = {
|
||||||
mx_GroupView_header: true,
|
mx_GroupView_header: true,
|
||||||
};
|
};
|
||||||
|
@ -404,15 +819,15 @@ export default React.createClass({
|
||||||
avatarNode = (
|
avatarNode = (
|
||||||
<div className="mx_GroupView_avatarPicker">
|
<div className="mx_GroupView_avatarPicker">
|
||||||
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||||
{avatarImage}
|
{ avatarImage }
|
||||||
</label>
|
</label>
|
||||||
<div className="mx_GroupView_avatarPicker_edit">
|
<div className="mx_GroupView_avatarPicker_edit">
|
||||||
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||||
<img src="img/camera.svg"
|
<img src="img/camera.svg"
|
||||||
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
|
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
||||||
width="17" height="15" />
|
width="17" height="15" />
|
||||||
</label>
|
</label>
|
||||||
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected}/>
|
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -428,20 +843,26 @@ export default React.createClass({
|
||||||
placeholder={_t('Description')}
|
placeholder={_t('Description')}
|
||||||
tabIndex="2"
|
tabIndex="2"
|
||||||
/>;
|
/>;
|
||||||
rightButtons = <span>
|
rightButtons.push(
|
||||||
<AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" onClick={this._onSaveClick}>
|
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
{_t('Save')}
|
onClick={this._onSaveClick} key="_saveButton"
|
||||||
</AccessibleButton>
|
>
|
||||||
<AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick}>
|
{ _t('Save') }
|
||||||
<img src="img/cancel.svg" className='mx_filterFlipColor'
|
</AccessibleButton>,
|
||||||
width="18" height="18" alt={_t("Cancel")}/>
|
);
|
||||||
</AccessibleButton>
|
rightButtons.push(
|
||||||
</span>;
|
<AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this._onCancelClick} key="_cancelButton">
|
||||||
|
<img src="img/cancel.svg" className="mx_filterFlipColor"
|
||||||
|
width="18" height="18" alt={_t("Cancel")} />
|
||||||
|
</AccessibleButton>,
|
||||||
|
);
|
||||||
roomBody = <div>
|
roomBody = <div>
|
||||||
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
|
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
|
||||||
onChange={this._onLongDescChange}
|
onChange={this._onLongDescChange}
|
||||||
tabIndex="3"
|
tabIndex="3"
|
||||||
/>
|
/>
|
||||||
|
{ this._getFeaturedRoomsNode() }
|
||||||
|
{ this._getFeaturedUsersNode() }
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
||||||
|
@ -452,31 +873,44 @@ export default React.createClass({
|
||||||
/>;
|
/>;
|
||||||
if (summary.profile && summary.profile.name) {
|
if (summary.profile && summary.profile.name) {
|
||||||
nameNode = <div>
|
nameNode = <div>
|
||||||
<span>{summary.profile.name}</span>
|
<span>{ summary.profile.name }</span>
|
||||||
<span className="mx_GroupView_header_groupid">
|
<span className="mx_GroupView_header_groupid">
|
||||||
({this.props.groupId})
|
({ this.props.groupId })
|
||||||
</span>
|
</span>
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
nameNode = <span>{this.props.groupId}</span>;
|
nameNode = <span>{ this.props.groupId }</span>;
|
||||||
}
|
}
|
||||||
shortDescNode = <span>{summary.profile.short_description}</span>;
|
shortDescNode = <span>{ summary.profile.short_description }</span>;
|
||||||
|
|
||||||
let description = null;
|
let description = null;
|
||||||
if (summary.profile && summary.profile.long_description) {
|
if (summary.profile && summary.profile.long_description) {
|
||||||
description = sanitizedHtmlNode(summary.profile.long_description);
|
description = sanitizedHtmlNode(summary.profile.long_description);
|
||||||
}
|
}
|
||||||
roomBody = <div>
|
roomBody = <div>
|
||||||
<div className="mx_GroupView_groupDesc">{description}</div>
|
{ this._getMembershipSection() }
|
||||||
{this._getFeaturedRoomsNode()}
|
<div className="mx_GroupView_groupDesc">{ description }</div>
|
||||||
{this._getFeaturedUsersNode()}
|
{ this._getFeaturedRoomsNode() }
|
||||||
|
{ this._getFeaturedUsersNode() }
|
||||||
</div>;
|
</div>;
|
||||||
// disabled until editing works
|
if (summary.user && summary.user.is_privileged) {
|
||||||
rightButtons = <AccessibleButton className="mx_GroupHeader_button"
|
rightButtons.push(
|
||||||
onClick={this._onEditClick} title={_t("Edit Group")}
|
<AccessibleButton className="mx_GroupHeader_button"
|
||||||
>
|
onClick={this._onEditClick} title={_t("Edit Group")} key="_editButton"
|
||||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
>
|
||||||
</AccessibleButton>;
|
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16" />
|
||||||
|
</AccessibleButton>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.props.collapsedRhs) {
|
||||||
|
rightButtons.push(
|
||||||
|
<AccessibleButton className="mx_GroupHeader_button"
|
||||||
|
onClick={this._onShowRhsClick} title={_t('Show panel')} key="_maximiseButton"
|
||||||
|
>
|
||||||
|
<TintableSvg src="img/maximise.svg" width="10" height="16" />
|
||||||
|
</AccessibleButton>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
headerClasses.mx_GroupView_header_view = true;
|
headerClasses.mx_GroupView_header_view = true;
|
||||||
}
|
}
|
||||||
|
@ -486,40 +920,40 @@ export default React.createClass({
|
||||||
<div className={classnames(headerClasses)}>
|
<div className={classnames(headerClasses)}>
|
||||||
<div className="mx_GroupView_header_leftCol">
|
<div className="mx_GroupView_header_leftCol">
|
||||||
<div className="mx_GroupView_header_avatar">
|
<div className="mx_GroupView_header_avatar">
|
||||||
{avatarNode}
|
{ avatarNode }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_GroupView_header_info">
|
<div className="mx_GroupView_header_info">
|
||||||
<div className="mx_GroupView_header_name">
|
<div className="mx_GroupView_header_name">
|
||||||
{nameNode}
|
{ nameNode }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_GroupView_header_shortDesc">
|
<div className="mx_GroupView_header_shortDesc">
|
||||||
{shortDescNode}
|
{ shortDescNode }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_GroupView_header_rightCol">
|
<div className="mx_GroupView_header_rightCol">
|
||||||
{rightButtons}
|
{ rightButtons }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{roomBody}
|
{ roomBody }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.state.error) {
|
} else if (this.state.error) {
|
||||||
if (this.state.error.httpStatus === 404) {
|
if (this.state.error.httpStatus === 404) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_GroupView_error">
|
<div className="mx_GroupView_error">
|
||||||
Group {this.props.groupId} not found
|
Group { this.props.groupId } not found
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let extraText;
|
let extraText;
|
||||||
if (this.state.error.errcode === 'M_UNRECOGNIZED') {
|
if (this.state.error.errcode === 'M_UNRECOGNIZED') {
|
||||||
extraText = <div>{_t('This Home server does not support groups')}</div>;
|
extraText = <div>{ _t('This Home server does not support groups') }</div>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_GroupView_error">
|
<div className="mx_GroupView_error">
|
||||||
Failed to load {this.props.groupId}
|
Failed to load { this.props.groupId }
|
||||||
{extraText}
|
{ extraText }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,10 +81,6 @@ export default React.createClass({
|
||||||
// stash the MatrixClient in case we log out before we are unmounted
|
// stash the MatrixClient in case we log out before we are unmounted
|
||||||
this._matrixClient = this.props.matrixClient;
|
this._matrixClient = this.props.matrixClient;
|
||||||
|
|
||||||
// _scrollStateMap is a map from room id to the scroll state returned by
|
|
||||||
// RoomView.getScrollState()
|
|
||||||
this._scrollStateMap = {};
|
|
||||||
|
|
||||||
CallMediaHandler.loadDevices();
|
CallMediaHandler.loadDevices();
|
||||||
|
|
||||||
document.addEventListener('keydown', this._onKeyDown);
|
document.addEventListener('keydown', this._onKeyDown);
|
||||||
|
@ -116,10 +112,6 @@ export default React.createClass({
|
||||||
return Boolean(MatrixClientPeg.get());
|
return Boolean(MatrixClientPeg.get());
|
||||||
},
|
},
|
||||||
|
|
||||||
getScrollStateForRoom: function(roomId) {
|
|
||||||
return this._scrollStateMap[roomId];
|
|
||||||
},
|
|
||||||
|
|
||||||
canResetTimelineInRoom: function(roomId) {
|
canResetTimelineInRoom: function(roomId) {
|
||||||
if (!this.refs.roomView) {
|
if (!this.refs.roomView) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -139,6 +131,9 @@ export default React.createClass({
|
||||||
useCompactLayout: event.getContent().useCompactLayout,
|
useCompactLayout: event.getContent().useCompactLayout,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (event.getType() === "m.ignored_user_list") {
|
||||||
|
dis.dispatch({action: "ignore_state_changed"});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onKeyDown: function(ev) {
|
_onKeyDown: function(ev) {
|
||||||
|
@ -246,11 +241,10 @@ export default React.createClass({
|
||||||
eventPixelOffset={this.props.initialEventPixelOffset}
|
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||||
key={this.props.currentRoomId || 'roomview'}
|
key={this.props.currentRoomId || 'roomview'}
|
||||||
opacity={this.props.middleOpacity}
|
opacity={this.props.middleOpacity}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapseRhs}
|
||||||
ConferenceHandler={this.props.ConferenceHandler}
|
ConferenceHandler={this.props.ConferenceHandler}
|
||||||
scrollStateMap={this._scrollStateMap}
|
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
|
if (!this.props.collapseRhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.UserSettings:
|
case PageTypes.UserSettings:
|
||||||
|
@ -261,7 +255,7 @@ export default React.createClass({
|
||||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||||
teamToken={this.props.teamToken}
|
teamToken={this.props.teamToken}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.MyGroups:
|
case PageTypes.MyGroups:
|
||||||
|
@ -271,9 +265,9 @@ export default React.createClass({
|
||||||
case PageTypes.CreateRoom:
|
case PageTypes.CreateRoom:
|
||||||
page_element = <CreateRoom
|
page_element = <CreateRoom
|
||||||
onRoomCreated={this.props.onRoomCreated}
|
onRoomCreated={this.props.onRoomCreated}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapseRhs}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.RoomDirectory:
|
case PageTypes.RoomDirectory:
|
||||||
|
@ -306,8 +300,9 @@ export default React.createClass({
|
||||||
case PageTypes.GroupView:
|
case PageTypes.GroupView:
|
||||||
page_element = <GroupView
|
page_element = <GroupView
|
||||||
groupId={this.props.currentGroupId}
|
groupId={this.props.currentGroupId}
|
||||||
|
collapsedRhs={this.props.collapseRhs}
|
||||||
/>;
|
/>;
|
||||||
//right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,7 +334,7 @@ export default React.createClass({
|
||||||
<div className={bodyClasses}>
|
<div className={bodyClasses}>
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
selectedRoom={this.props.currentRoomId}
|
selectedRoom={this.props.currentRoomId}
|
||||||
collapsed={this.props.collapse_lhs || false}
|
collapsed={this.props.collapseLhs || false}
|
||||||
opacity={this.props.leftOpacity}
|
opacity={this.props.leftOpacity}
|
||||||
/>
|
/>
|
||||||
<main className='mx_MatrixChat_middlePanel'>
|
<main className='mx_MatrixChat_middlePanel'>
|
||||||
|
|
|
@ -32,13 +32,12 @@ import dis from "../../dispatcher";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import Tinter from "../../Tinter";
|
import Tinter from "../../Tinter";
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite';
|
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite';
|
||||||
import * as Rooms from '../../Rooms';
|
import * as Rooms from '../../Rooms';
|
||||||
import linkifyMatrix from "../../linkify-matrix";
|
import linkifyMatrix from "../../linkify-matrix";
|
||||||
import * as Lifecycle from '../../Lifecycle';
|
import * as Lifecycle from '../../Lifecycle';
|
||||||
// LifecycleStore is not used but does listen to and dispatch actions
|
// LifecycleStore is not used but does listen to and dispatch actions
|
||||||
require('../../stores/LifecycleStore');
|
require('../../stores/LifecycleStore');
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
|
|
||||||
import createRoom from "../../createRoom";
|
import createRoom from "../../createRoom";
|
||||||
|
@ -144,8 +143,8 @@ module.exports = React.createClass({
|
||||||
// If we're trying to just view a user ID (i.e. /user URL), this is it
|
// If we're trying to just view a user ID (i.e. /user URL), this is it
|
||||||
viewUserId: null,
|
viewUserId: null,
|
||||||
|
|
||||||
collapse_lhs: false,
|
collapseLhs: false,
|
||||||
collapse_rhs: false,
|
collapseRhs: false,
|
||||||
leftOpacity: 1.0,
|
leftOpacity: 1.0,
|
||||||
middleOpacity: 1.0,
|
middleOpacity: 1.0,
|
||||||
rightOpacity: 1.0,
|
rightOpacity: 1.0,
|
||||||
|
@ -214,9 +213,6 @@ module.exports = React.createClass({
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
SdkConfig.put(this.props.config);
|
SdkConfig.put(this.props.config);
|
||||||
|
|
||||||
this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated);
|
|
||||||
this._onRoomViewStoreUpdated();
|
|
||||||
|
|
||||||
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
|
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
|
||||||
|
|
||||||
// Used by _viewRoom before getting state from sync
|
// Used by _viewRoom before getting state from sync
|
||||||
|
@ -353,7 +349,6 @@ module.exports = React.createClass({
|
||||||
UDEHandler.stopListening();
|
UDEHandler.stopListening();
|
||||||
window.removeEventListener("focus", this.onFocus);
|
window.removeEventListener("focus", this.onFocus);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
this._roomViewStoreToken.remove();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
|
@ -439,7 +434,7 @@ module.exports = React.createClass({
|
||||||
break;
|
break;
|
||||||
case 'view_user':
|
case 'view_user':
|
||||||
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
|
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
|
||||||
if (this.state.collapse_rhs) {
|
if (this.state.collapseRhs) {
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'show_right_panel',
|
action: 'show_right_panel',
|
||||||
|
@ -521,22 +516,22 @@ module.exports = React.createClass({
|
||||||
break;
|
break;
|
||||||
case 'hide_left_panel':
|
case 'hide_left_panel':
|
||||||
this.setState({
|
this.setState({
|
||||||
collapse_lhs: true,
|
collapseLhs: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'show_left_panel':
|
case 'show_left_panel':
|
||||||
this.setState({
|
this.setState({
|
||||||
collapse_lhs: false,
|
collapseLhs: false,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'hide_right_panel':
|
case 'hide_right_panel':
|
||||||
this.setState({
|
this.setState({
|
||||||
collapse_rhs: true,
|
collapseRhs: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'show_right_panel':
|
case 'show_right_panel':
|
||||||
this.setState({
|
this.setState({
|
||||||
collapse_rhs: false,
|
collapseRhs: false,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'ui_opacity': {
|
case 'ui_opacity': {
|
||||||
|
@ -587,10 +582,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onRoomViewStoreUpdated: function() {
|
|
||||||
this.setState({ currentRoomId: RoomViewStore.getRoomId() });
|
|
||||||
},
|
|
||||||
|
|
||||||
_setPage: function(pageType) {
|
_setPage: function(pageType) {
|
||||||
this.setState({
|
this.setState({
|
||||||
page_type: pageType,
|
page_type: pageType,
|
||||||
|
@ -677,10 +668,10 @@ module.exports = React.createClass({
|
||||||
this.focusComposer = true;
|
this.focusComposer = true;
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
|
currentRoomId: roomInfo.room_id || null,
|
||||||
page_type: PageTypes.RoomView,
|
page_type: PageTypes.RoomView,
|
||||||
thirdPartyInvite: roomInfo.third_party_invite,
|
thirdPartyInvite: roomInfo.third_party_invite,
|
||||||
roomOobData: roomInfo.oob_data,
|
roomOobData: roomInfo.oob_data,
|
||||||
autoJoin: roomInfo.auto_join,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (roomInfo.room_alias) {
|
if (roomInfo.room_alias) {
|
||||||
|
@ -860,7 +851,7 @@ module.exports = React.createClass({
|
||||||
title: _t("Leave room"),
|
title: _t("Leave room"),
|
||||||
description: (
|
description: (
|
||||||
<span>
|
<span>
|
||||||
{_t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name})}
|
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onFinished: (shouldLeave) => {
|
onFinished: (shouldLeave) => {
|
||||||
|
@ -1000,8 +991,8 @@ module.exports = React.createClass({
|
||||||
this.setStateForNewView({
|
this.setStateForNewView({
|
||||||
view: VIEWS.LOGIN,
|
view: VIEWS.LOGIN,
|
||||||
ready: false,
|
ready: false,
|
||||||
collapse_lhs: false,
|
collapseLhs: false,
|
||||||
collapse_rhs: false,
|
collapseRhs: false,
|
||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
page_type: PageTypes.RoomDirectory,
|
page_type: PageTypes.RoomDirectory,
|
||||||
});
|
});
|
||||||
|
@ -1066,10 +1057,13 @@ module.exports = React.createClass({
|
||||||
self.setState({ready: true});
|
self.setState({ready: true});
|
||||||
});
|
});
|
||||||
cli.on('Call.incoming', function(call) {
|
cli.on('Call.incoming', function(call) {
|
||||||
|
// we dispatch this synchronously to make sure that the event
|
||||||
|
// handlers on the call are set up immediately (so that if
|
||||||
|
// we get an immediate hangup, we don't get a stuck call)
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'incoming_call',
|
action: 'incoming_call',
|
||||||
call: call,
|
call: call,
|
||||||
});
|
}, true);
|
||||||
});
|
});
|
||||||
cli.on('Session.logged_out', function(call) {
|
cli.on('Session.logged_out', function(call) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -1454,7 +1448,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_MatrixChat_splash">
|
<div className="mx_MatrixChat_splash">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
|
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
|
||||||
{ _t('Logout') }
|
{ _t('Logout') }
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -65,7 +65,7 @@ module.exports = React.createClass({
|
||||||
suppressFirstDateSeparator: React.PropTypes.bool,
|
suppressFirstDateSeparator: React.PropTypes.bool,
|
||||||
|
|
||||||
// whether to show read receipts
|
// whether to show read receipts
|
||||||
manageReadReceipts: React.PropTypes.bool,
|
showReadReceipts: React.PropTypes.bool,
|
||||||
|
|
||||||
// true if updates to the event list should cause the scroll panel to
|
// true if updates to the event list should cause the scroll panel to
|
||||||
// scroll down when we are at the bottom of the window. See ScrollPanel
|
// scroll down when we are at the bottom of the window. See ScrollPanel
|
||||||
|
@ -241,6 +241,10 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// TODO: Implement granular (per-room) hide options
|
// TODO: Implement granular (per-room) hide options
|
||||||
_shouldShowEvent: function(mxEv) {
|
_shouldShowEvent: function(mxEv) {
|
||||||
|
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
|
||||||
|
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||||
|
}
|
||||||
|
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
return false; // no tile = no show
|
return false; // no tile = no show
|
||||||
|
@ -339,6 +343,15 @@ module.exports = React.createClass({
|
||||||
for (;i + 1 < this.props.events.length; i++) {
|
for (;i + 1 < this.props.events.length; i++) {
|
||||||
const collapsedMxEv = this.props.events[i + 1];
|
const collapsedMxEv = this.props.events[i + 1];
|
||||||
|
|
||||||
|
// Ignore redacted/hidden member events
|
||||||
|
if (!this._shouldShowEvent(collapsedMxEv)) {
|
||||||
|
// If this hidden event is the RM and in or at end of a MELS put RM after MELS.
|
||||||
|
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
|
||||||
|
readMarkerInMels = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isMembershipChange(collapsedMxEv) ||
|
if (!isMembershipChange(collapsedMxEv) ||
|
||||||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
||||||
break;
|
break;
|
||||||
|
@ -349,16 +362,16 @@ module.exports = React.createClass({
|
||||||
readMarkerInMels = true;
|
readMarkerInMels = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore redacted/hidden member events
|
|
||||||
if (!this._shouldShowEvent(collapsedMxEv)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
summarisedEvents.push(collapsedMxEv);
|
summarisedEvents.push(collapsedMxEv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let highlightInMels = false;
|
||||||
|
|
||||||
// At this point, i = the index of the last event in the summary sequence
|
// At this point, i = the index of the last event in the summary sequence
|
||||||
let eventTiles = summarisedEvents.map((e) => {
|
let eventTiles = summarisedEvents.map((e) => {
|
||||||
|
if (e.getId() === this.props.highlightedEventId) {
|
||||||
|
highlightInMels = true;
|
||||||
|
}
|
||||||
// In order to prevent DateSeparators from appearing in the expanded form
|
// In order to prevent DateSeparators from appearing in the expanded form
|
||||||
// of MemberEventListSummary, render each member event as if the previous
|
// of MemberEventListSummary, render each member event as if the previous
|
||||||
// one was itself. This way, the timestamp of the previous event === the
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
|
@ -372,15 +385,13 @@ module.exports = React.createClass({
|
||||||
eventTiles = null;
|
eventTiles = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.push(
|
ret.push(<MemberEventListSummary key={key}
|
||||||
<MemberEventListSummary
|
events={summarisedEvents}
|
||||||
key={key}
|
onToggle={this._onWidgetLoad} // Update scroll state
|
||||||
events={summarisedEvents}
|
startExpanded={highlightInMels}
|
||||||
onToggle={this._onWidgetLoad} // Update scroll state
|
>
|
||||||
>
|
{eventTiles}
|
||||||
{eventTiles}
|
</MemberEventListSummary>);
|
||||||
</MemberEventListSummary>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (readMarkerInMels) {
|
if (readMarkerInMels) {
|
||||||
ret.push(this._getReadMarkerTile(visible));
|
ret.push(this._getReadMarkerTile(visible));
|
||||||
|
@ -487,7 +498,7 @@ module.exports = React.createClass({
|
||||||
var scrollToken = mxEv.status ? undefined : eventId;
|
var scrollToken = mxEv.status ? undefined : eventId;
|
||||||
|
|
||||||
var readReceipts;
|
var readReceipts;
|
||||||
if (this.props.manageReadReceipts) {
|
if (this.props.showReadReceipts) {
|
||||||
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
||||||
}
|
}
|
||||||
ret.push(
|
ret.push(
|
||||||
|
@ -545,6 +556,9 @@ module.exports = React.createClass({
|
||||||
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
|
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
|
||||||
return; // ignore non-read receipts and receipts from self.
|
return; // ignore non-read receipts and receipts from self.
|
||||||
}
|
}
|
||||||
|
if (MatrixClientPeg.get().isUserIgnored(r.userId)) {
|
||||||
|
return; // ignore ignored users
|
||||||
|
}
|
||||||
let member = room.getMember(r.userId);
|
let member = room.getMember(r.userId);
|
||||||
if (!member) {
|
if (!member) {
|
||||||
return; // ignore unknown user IDs
|
return; // ignore unknown user IDs
|
||||||
|
|
|
@ -39,7 +39,7 @@ const GroupTile = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return <a onClick={this.onClick} href="#">{this.props.groupId}</a>;
|
return <a onClick={this.onClick} href="#">{ this.props.groupId }</a>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -90,51 +90,51 @@ export default withMatrixClient(React.createClass({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
content = <div>
|
content = <div>
|
||||||
<div>{_t('You are a member of these groups:')}</div>
|
<div>{ _t('You are a member of these groups:') }</div>
|
||||||
{groupNodes}
|
{ groupNodes }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.error) {
|
} else if (this.state.error) {
|
||||||
content = <div className="mx_MyGroups_error">
|
content = <div className="mx_MyGroups_error">
|
||||||
{_t('Error whilst fetching joined groups')}
|
{ _t('Error whilst fetching joined groups') }
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
content = <Loader />;
|
content = <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_MyGroups">
|
return <div className="mx_MyGroups">
|
||||||
<SimpleRoomHeader title={ _t("Groups") } />
|
<SimpleRoomHeader title={_t("Groups")} icon="img/icons-groups.svg" />
|
||||||
<div className='mx_MyGroups_joinCreateBox'>
|
<div className='mx_MyGroups_joinCreateBox'>
|
||||||
<div className="mx_MyGroups_createBox">
|
<div className="mx_MyGroups_createBox">
|
||||||
<div className="mx_MyGroups_joinCreateHeader">
|
<div className="mx_MyGroups_joinCreateHeader">
|
||||||
{_t('Create a new group')}
|
{ _t('Create a new group') }
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
|
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
|
||||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{_t(
|
{ _t(
|
||||||
'Create a group to represent your community! '+
|
'Create a group to represent your community! '+
|
||||||
'Define a set of rooms and your own custom homepage '+
|
'Define a set of rooms and your own custom homepage '+
|
||||||
'to mark out your space in the Matrix universe.',
|
'to mark out your space in the Matrix universe.',
|
||||||
)}
|
) }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MyGroups_joinBox">
|
<div className="mx_MyGroups_joinBox">
|
||||||
<div className="mx_MyGroups_joinCreateHeader">
|
<div className="mx_MyGroups_joinCreateHeader">
|
||||||
{_t('Join an existing group')}
|
{ _t('Join an existing group') }
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
|
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
|
||||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{_tJsx(
|
{ _tJsx(
|
||||||
'To join an exisitng group you\'ll have to '+
|
'To join an existing group you\'ll have to '+
|
||||||
'know its group identifier; this will look '+
|
'know its group identifier; this will look '+
|
||||||
'something like <i>+example:matrix.org</i>.',
|
'something like <i>+example:matrix.org</i>.',
|
||||||
/<i>(.*)<\/i>/,
|
/<i>(.*)<\/i>/,
|
||||||
(sub) => <i>{sub}</i>,
|
(sub) => <i>{ sub }</i>,
|
||||||
)}
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MyGroups_content">
|
<div className="mx_MyGroups_content">
|
||||||
{content}
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
|
@ -121,7 +121,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onRoomMemberTyping: function(ev, member) {
|
onRoomMemberTyping: function(ev, member) {
|
||||||
this.setState({
|
this.setState({
|
||||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
usersTyping: WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ limitations under the License.
|
||||||
// - Drag and drop
|
// - Drag and drop
|
||||||
// - File uploading - uploadFile()
|
// - File uploading - uploadFile()
|
||||||
|
|
||||||
|
import shouldHideEvent from "../../shouldHideEvent";
|
||||||
|
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
@ -45,6 +47,7 @@ import KeyCode from '../../KeyCode';
|
||||||
import UserProvider from '../../autocomplete/UserProvider';
|
import UserProvider from '../../autocomplete/UserProvider';
|
||||||
|
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
|
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||||
|
|
||||||
let DEBUG = false;
|
let DEBUG = false;
|
||||||
let debuglog = function() {};
|
let debuglog = function() {};
|
||||||
|
@ -120,6 +123,9 @@ module.exports = React.createClass({
|
||||||
// store the error here.
|
// store the error here.
|
||||||
roomLoadError: null,
|
roomLoadError: null,
|
||||||
|
|
||||||
|
// Have we sent a request to join the room that we're waiting to complete?
|
||||||
|
joining: false,
|
||||||
|
|
||||||
// this is true if we are fully scrolled-down, and are looking at
|
// this is true if we are fully scrolled-down, and are looking at
|
||||||
// the end of the live timeline. It has the effect of hiding the
|
// the end of the live timeline. It has the effect of hiding the
|
||||||
// 'scroll to bottom' knob, among a couple of other things.
|
// 'scroll to bottom' knob, among a couple of other things.
|
||||||
|
@ -143,6 +149,8 @@ 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);
|
||||||
|
@ -152,6 +160,22 @@ module.exports = React.createClass({
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!initial && this.state.roomId !== RoomViewStore.getRoomId()) {
|
||||||
|
// RoomView explicitly does not support changing what room
|
||||||
|
// is being viewed: instead it should just be re-mounted when
|
||||||
|
// switching rooms. Therefore, if the room ID changes, we
|
||||||
|
// ignore this. We either need to do this or add code to handle
|
||||||
|
// saving the scroll position (otherwise we end up saving the
|
||||||
|
// scroll position against the wrong room).
|
||||||
|
|
||||||
|
// Given that doing the setState here would cause a bunch of
|
||||||
|
// unnecessary work, we just ignore the change since we know
|
||||||
|
// that if the current room ID has changed from what we thought
|
||||||
|
// it was, it means we're about to be unmounted.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId: RoomViewStore.getRoomId(),
|
||||||
roomAlias: RoomViewStore.getRoomAlias(),
|
roomAlias: RoomViewStore.getRoomAlias(),
|
||||||
|
@ -159,16 +183,11 @@ module.exports = React.createClass({
|
||||||
roomLoadError: RoomViewStore.getRoomLoadError(),
|
roomLoadError: RoomViewStore.getRoomLoadError(),
|
||||||
joining: RoomViewStore.isJoining(),
|
joining: RoomViewStore.isJoining(),
|
||||||
initialEventId: RoomViewStore.getInitialEventId(),
|
initialEventId: RoomViewStore.getInitialEventId(),
|
||||||
initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
|
|
||||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||||
shouldPeek: RoomViewStore.shouldPeek(),
|
shouldPeek: RoomViewStore.shouldPeek(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// finished joining, start waiting for a room and show a spinner. See onRoom.
|
|
||||||
newState.waitingForRoom = this.state.joining && !newState.joining &&
|
|
||||||
!RoomViewStore.getJoinError();
|
|
||||||
|
|
||||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||||
console.log(
|
console.log(
|
||||||
'RVS update:',
|
'RVS update:',
|
||||||
|
@ -177,7 +196,6 @@ module.exports = React.createClass({
|
||||||
'loading?', newState.roomLoading,
|
'loading?', newState.roomLoading,
|
||||||
'joining?', newState.joining,
|
'joining?', newState.joining,
|
||||||
'initial?', initial,
|
'initial?', initial,
|
||||||
'waiting?', newState.waitingForRoom,
|
|
||||||
'shouldPeek?', newState.shouldPeek,
|
'shouldPeek?', newState.shouldPeek,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -185,6 +203,25 @@ module.exports = React.createClass({
|
||||||
// the RoomView instance
|
// the RoomView instance
|
||||||
if (initial) {
|
if (initial) {
|
||||||
newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
|
newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
|
||||||
|
if (newState.room) {
|
||||||
|
newState.unsentMessageError = this._getUnsentMessageError(newState.room);
|
||||||
|
newState.showApps = this._shouldShowApps(newState.room);
|
||||||
|
this._onRoomLoaded(newState.room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.roomId === null && newState.roomId !== null) {
|
||||||
|
// Get the scroll state for the new room
|
||||||
|
|
||||||
|
// If an event ID wasn't specified, default to the one saved for this room
|
||||||
|
// in the scroll state store. Assume initialEventPixelOffset should be set.
|
||||||
|
if (!newState.initialEventId) {
|
||||||
|
const roomScrollState = RoomScrollStateStore.getScrollState(newState.roomId);
|
||||||
|
if (roomScrollState) {
|
||||||
|
newState.initialEventId = roomScrollState.focussedEvent;
|
||||||
|
newState.initialEventPixelOffset = roomScrollState.pixelOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the search results when clicking a search result (which changes the
|
// Clear the search results when clicking a search result (which changes the
|
||||||
|
@ -193,22 +230,20 @@ module.exports = React.createClass({
|
||||||
newState.searchResults = null;
|
newState.searchResults = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the scroll state for the previous room so that we can return to this
|
this.setState(newState);
|
||||||
// position when viewing this room in future.
|
// At this point, newState.roomId could be null (e.g. the alias might not
|
||||||
if (this.state.roomId !== newState.roomId) {
|
// have been resolved yet) so anything called here must handle this case.
|
||||||
this._updateScrollMap(this.state.roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(newState, () => {
|
// We pass the new state into this function for it to read: it needs to
|
||||||
// At this point, this.state.roomId could be null (e.g. the alias might not
|
// observe the new state but we don't want to put it in the setState
|
||||||
// have been resolved yet) so anything called here must handle this case.
|
// callback because this would prevent the setStates from being batched,
|
||||||
if (initial) {
|
// ie. cause it to render RoomView twice rather than the once that is necessary.
|
||||||
this._onHaveRoom();
|
if (initial) {
|
||||||
}
|
this._setupRoom(newState.room, newState.roomId, newState.joining, newState.shouldPeek);
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onHaveRoom: function() {
|
_setupRoom: function(room, roomId, joining, shouldPeek) {
|
||||||
// if this is an unknown room then we're in one of three states:
|
// if this is an unknown room then we're in one of three states:
|
||||||
// - This is a room we can peek into (search engine) (we can /peek)
|
// - This is a room we can peek into (search engine) (we can /peek)
|
||||||
// - This is a room we can publicly join or were invited to. (we can /join)
|
// - This is a room we can publicly join or were invited to. (we can /join)
|
||||||
|
@ -224,23 +259,15 @@ module.exports = React.createClass({
|
||||||
// about it). We don't peek in the historical case where we were joined but are
|
// about it). We don't peek in the historical case where we were joined but are
|
||||||
// now not joined because the js-sdk peeking API will clobber our historical room,
|
// now not joined because the js-sdk peeking API will clobber our historical room,
|
||||||
// making it impossible to indicate a newly joined room.
|
// making it impossible to indicate a newly joined room.
|
||||||
const room = this.state.room;
|
if (!joining && roomId) {
|
||||||
if (room) {
|
|
||||||
this.setState({
|
|
||||||
unsentMessageError: this._getUnsentMessageError(room),
|
|
||||||
showApps: this._shouldShowApps(room),
|
|
||||||
});
|
|
||||||
this._onRoomLoaded(room);
|
|
||||||
}
|
|
||||||
if (!this.state.joining && this.state.roomId) {
|
|
||||||
if (this.props.autoJoin) {
|
if (this.props.autoJoin) {
|
||||||
this.onJoinButtonClicked();
|
this.onJoinButtonClicked();
|
||||||
} else if (!room && this.state.shouldPeek) {
|
} else if (!room && shouldPeek) {
|
||||||
console.log("Attempting to peek into room %s", this.state.roomId);
|
console.log("Attempting to peek into room %s", roomId);
|
||||||
this.setState({
|
this.setState({
|
||||||
peekLoading: true,
|
peekLoading: true,
|
||||||
});
|
});
|
||||||
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
|
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
room: room,
|
room: room,
|
||||||
peekLoading: false,
|
peekLoading: false,
|
||||||
|
@ -336,7 +363,9 @@ module.exports = React.createClass({
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
|
|
||||||
// update the scroll map before we get unmounted
|
// update the scroll map before we get unmounted
|
||||||
this._updateScrollMap(this.state.roomId);
|
if (this.state.roomId) {
|
||||||
|
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
|
||||||
|
}
|
||||||
|
|
||||||
if (this.refs.roomView) {
|
if (this.refs.roomView) {
|
||||||
// disconnect the D&D event listeners from the room view. This
|
// disconnect the D&D event listeners from the room view. This
|
||||||
|
@ -497,8 +526,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 {
|
|
||||||
this.setState((state, props) => {
|
this.setState((state, props) => {
|
||||||
return {numUnreadMessages: state.numUnreadMessages + 1};
|
return {numUnreadMessages: state.numUnreadMessages + 1};
|
||||||
});
|
});
|
||||||
|
@ -614,25 +642,12 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateScrollMap(roomId) {
|
|
||||||
// No point updating scroll state if the room ID hasn't been resolved yet
|
|
||||||
if (!roomId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'update_scroll_state',
|
|
||||||
room_id: roomId,
|
|
||||||
scroll_state: this._getScrollState(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoom: function(room) {
|
onRoom: function(room) {
|
||||||
if (!room || room.roomId !== this.state.roomId) {
|
if (!room || room.roomId !== this.state.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
room: room,
|
room: room,
|
||||||
waitingForRoom: false,
|
|
||||||
}, () => {
|
}, () => {
|
||||||
this._onRoomLoaded(room);
|
this._onRoomLoaded(room);
|
||||||
});
|
});
|
||||||
|
@ -688,14 +703,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onRoomMemberMembership: function(ev, member, oldMembership) {
|
onRoomMemberMembership: function(ev, member, oldMembership) {
|
||||||
if (member.userId == MatrixClientPeg.get().credentials.userId) {
|
if (member.userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
|
this.forceUpdate();
|
||||||
if (member.membership === 'join') {
|
|
||||||
this.setState({
|
|
||||||
waitingForRoom: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.forceUpdate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1445,10 +1453,6 @@ module.exports = React.createClass({
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||||
|
|
||||||
// Whether the preview bar spinner should be shown. We do this when joining or
|
|
||||||
// when waiting for a room to be returned by js-sdk when joining
|
|
||||||
const previewBarSpinner = this.state.joining || this.state.waitingForRoom;
|
|
||||||
|
|
||||||
if (!this.state.room) {
|
if (!this.state.room) {
|
||||||
if (this.state.roomLoading || this.state.peekLoading) {
|
if (this.state.roomLoading || this.state.peekLoading) {
|
||||||
return (
|
return (
|
||||||
|
@ -1482,7 +1486,7 @@ module.exports = React.createClass({
|
||||||
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
|
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
|
||||||
canPreview={ false } error={ this.state.roomLoadError }
|
canPreview={ false } error={ this.state.roomLoadError }
|
||||||
roomAlias={roomAlias}
|
roomAlias={roomAlias}
|
||||||
spinner={previewBarSpinner}
|
spinner={this.state.joining}
|
||||||
inviterName={inviterName}
|
inviterName={inviterName}
|
||||||
invitedEmail={invitedEmail}
|
invitedEmail={invitedEmail}
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
|
@ -1525,7 +1529,7 @@ module.exports = React.createClass({
|
||||||
onRejectClick={ this.onRejectButtonClicked }
|
onRejectClick={ this.onRejectButtonClicked }
|
||||||
inviterName={ inviterName }
|
inviterName={ inviterName }
|
||||||
canPreview={ false }
|
canPreview={ false }
|
||||||
spinner={previewBarSpinner}
|
spinner={this.state.joining}
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1600,7 +1604,7 @@ module.exports = React.createClass({
|
||||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||||
onForgetClick={ this.onForgetClick }
|
onForgetClick={ this.onForgetClick }
|
||||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||||
spinner={previewBarSpinner}
|
spinner={this.state.joining}
|
||||||
inviterName={inviterName}
|
inviterName={inviterName}
|
||||||
invitedEmail={invitedEmail}
|
invitedEmail={invitedEmail}
|
||||||
canPreview={this.state.canPeek}
|
canPreview={this.state.canPeek}
|
||||||
|
@ -1716,7 +1720,8 @@ module.exports = React.createClass({
|
||||||
var messagePanel = (
|
var messagePanel = (
|
||||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||||
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
showReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
||||||
|
manageReadReceipts={true}
|
||||||
manageReadMarkers={true}
|
manageReadMarkers={true}
|
||||||
hidden={hideMessagePanel}
|
hidden={hideMessagePanel}
|
||||||
highlightedEventId={highlightedEventId}
|
highlightedEventId={highlightedEventId}
|
||||||
|
|
|
@ -157,7 +157,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.checkFillState();
|
this.checkScroll();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
|
|
|
@ -59,6 +59,7 @@ var TimelinePanel = React.createClass({
|
||||||
// that room.
|
// that room.
|
||||||
timelineSet: React.PropTypes.object.isRequired,
|
timelineSet: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
showReadReceipts: React.PropTypes.bool,
|
||||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||||
manageReadReceipts: React.PropTypes.bool,
|
manageReadReceipts: React.PropTypes.bool,
|
||||||
manageReadMarkers: React.PropTypes.bool,
|
manageReadMarkers: React.PropTypes.bool,
|
||||||
|
@ -197,6 +198,7 @@ var TimelinePanel = React.createClass({
|
||||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||||
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
||||||
|
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
|
||||||
MatrixClientPeg.get().on("sync", this.onSync);
|
MatrixClientPeg.get().on("sync", this.onSync);
|
||||||
|
|
||||||
this._initTimeline(this.props);
|
this._initTimeline(this.props);
|
||||||
|
@ -266,6 +268,7 @@ var TimelinePanel = React.createClass({
|
||||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||||
client.removeListener("Room.accountData", this.onAccountData);
|
client.removeListener("Room.accountData", this.onAccountData);
|
||||||
|
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||||
client.removeListener("sync", this.onSync);
|
client.removeListener("sync", this.onSync);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -341,9 +344,16 @@ var TimelinePanel = React.createClass({
|
||||||
newState[canPaginateOtherWayKey] = true;
|
newState[canPaginateOtherWayKey] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(newState);
|
// Don't resolve until the setState has completed: we need to let
|
||||||
|
// the component update before we consider the pagination completed,
|
||||||
return r;
|
// otherwise we'll end up paginating in all the history the js-sdk
|
||||||
|
// has in memory because we never gave the component a chance to scroll
|
||||||
|
// itself into the right place
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.setState(newState, () => {
|
||||||
|
resolve(r);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -374,6 +384,9 @@ var TimelinePanel = React.createClass({
|
||||||
this.sendReadReceipt();
|
this.sendReadReceipt();
|
||||||
this.updateReadMarker();
|
this.updateReadMarker();
|
||||||
break;
|
break;
|
||||||
|
case 'ignore_state_changed':
|
||||||
|
this.forceUpdate();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -503,6 +516,18 @@ var TimelinePanel = React.createClass({
|
||||||
}, this.props.onReadMarkerUpdated);
|
}, this.props.onReadMarkerUpdated);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onEventDecrypted: function(ev) {
|
||||||
|
// Need to update as we don't display event tiles for events that
|
||||||
|
// haven't yet been decrypted. The event will have just been updated
|
||||||
|
// in place so we just need to re-render.
|
||||||
|
// TODO: We should restrict this to only events in our timeline,
|
||||||
|
// but possibly the event tile itself should just update when this
|
||||||
|
// happens to save us re-rendering the whole timeline.
|
||||||
|
if (ev.getRoomId() === this.props.timelineSet.room.roomId) {
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onSync: function(state, prevState, data) {
|
onSync: function(state, prevState, data) {
|
||||||
this.setState({clientSyncState: state});
|
this.setState({clientSyncState: state});
|
||||||
},
|
},
|
||||||
|
@ -1126,8 +1151,8 @@ var TimelinePanel = React.createClass({
|
||||||
readMarkerEventId={ this.state.readMarkerEventId }
|
readMarkerEventId={ this.state.readMarkerEventId }
|
||||||
readMarkerVisible={ this.state.readMarkerVisible }
|
readMarkerVisible={ this.state.readMarkerVisible }
|
||||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||||
showUrlPreview = { this.props.showUrlPreview }
|
showUrlPreview={ this.props.showUrlPreview }
|
||||||
manageReadReceipts = { this.props.manageReadReceipts }
|
showReadReceipts={ this.props.showReadReceipts }
|
||||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||||
stickyBottom={ stickyBottom }
|
stickyBottom={ stickyBottom }
|
||||||
onScroll={ this.onMessageListScroll }
|
onScroll={ this.onMessageListScroll }
|
||||||
|
|
|
@ -32,7 +32,7 @@ const AddThreepid = require('../../AddThreepid');
|
||||||
const SdkConfig = require('../../SdkConfig');
|
const SdkConfig = require('../../SdkConfig');
|
||||||
import Analytics from '../../Analytics';
|
import Analytics from '../../Analytics';
|
||||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t, _td } from '../../languageHandler';
|
||||||
import * as languageHandler from '../../languageHandler';
|
import * as languageHandler from '../../languageHandler';
|
||||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ const gHVersionLabel = function(repo, token='') {
|
||||||
} else {
|
} else {
|
||||||
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
|
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
|
||||||
}
|
}
|
||||||
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).
|
||||||
|
@ -63,51 +63,55 @@ const gHVersionLabel = function(repo, token='') {
|
||||||
const SETTINGS_LABELS = [
|
const SETTINGS_LABELS = [
|
||||||
{
|
{
|
||||||
id: 'autoplayGifsAndVideos',
|
id: 'autoplayGifsAndVideos',
|
||||||
label: 'Autoplay GIFs and videos',
|
label: _td('Autoplay GIFs and videos'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideReadReceipts',
|
id: 'hideReadReceipts',
|
||||||
label: 'Hide read receipts',
|
label: _td('Hide read receipts'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dontSendTypingNotifications',
|
id: 'dontSendTypingNotifications',
|
||||||
label: "Don't send typing notifications",
|
label: _td("Don't send typing notifications"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alwaysShowTimestamps',
|
id: 'alwaysShowTimestamps',
|
||||||
label: 'Always show message timestamps',
|
label: _td('Always show message timestamps'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'showTwelveHourTimestamps',
|
id: 'showTwelveHourTimestamps',
|
||||||
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
label: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideJoinLeaves',
|
id: 'hideJoinLeaves',
|
||||||
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
|
label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideAvatarDisplaynameChanges',
|
id: 'hideAvatarDisplaynameChanges',
|
||||||
label: 'Hide avatar and display name changes',
|
label: _td('Hide avatar and display name changes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'useCompactLayout',
|
id: 'useCompactLayout',
|
||||||
label: 'Use compact timeline layout',
|
label: _td('Use compact timeline layout'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideRedactions',
|
id: 'hideRedactions',
|
||||||
label: 'Hide removed messages',
|
label: _td('Hide removed messages'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'enableSyntaxHighlightLanguageDetection',
|
id: 'enableSyntaxHighlightLanguageDetection',
|
||||||
label: 'Enable automatic language detection for syntax highlighting',
|
label: _td('Enable automatic language detection for syntax highlighting'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'MessageComposerInput.autoReplaceEmoji',
|
id: 'MessageComposerInput.autoReplaceEmoji',
|
||||||
label: 'Automatically replace plain text Emoji',
|
label: _td('Automatically replace plain text Emoji'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MessageComposerInput.dontSuggestEmoji',
|
||||||
|
label: _td('Disable Emoji suggestions while typing'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Pill.shouldHidePillAvatar',
|
id: 'Pill.shouldHidePillAvatar',
|
||||||
label: 'Hide avatars in user and room mentions',
|
label: _td('Hide avatars in user and room mentions'),
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
|
@ -120,7 +124,7 @@ const SETTINGS_LABELS = [
|
||||||
const ANALYTICS_SETTINGS_LABELS = [
|
const ANALYTICS_SETTINGS_LABELS = [
|
||||||
{
|
{
|
||||||
id: 'analyticsOptOut',
|
id: 'analyticsOptOut',
|
||||||
label: 'Opt out of analytics',
|
label: _td('Opt out of analytics'),
|
||||||
fn: function(checked) {
|
fn: function(checked) {
|
||||||
Analytics[checked ? 'disable' : 'enable']();
|
Analytics[checked ? 'disable' : 'enable']();
|
||||||
},
|
},
|
||||||
|
@ -130,7 +134,7 @@ const ANALYTICS_SETTINGS_LABELS = [
|
||||||
const WEBRTC_SETTINGS_LABELS = [
|
const WEBRTC_SETTINGS_LABELS = [
|
||||||
{
|
{
|
||||||
id: 'webRtcForceTURN',
|
id: 'webRtcForceTURN',
|
||||||
label: 'Disable Peer-to-Peer for 1:1 calls',
|
label: _td('Disable Peer-to-Peer for 1:1 calls'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -139,7 +143,7 @@ const WEBRTC_SETTINGS_LABELS = [
|
||||||
const CRYPTO_SETTINGS_LABELS = [
|
const CRYPTO_SETTINGS_LABELS = [
|
||||||
{
|
{
|
||||||
id: 'blacklistUnverifiedDevices',
|
id: 'blacklistUnverifiedDevices',
|
||||||
label: 'Never send encrypted messages to unverified devices from this device',
|
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);
|
||||||
},
|
},
|
||||||
|
@ -162,16 +166,44 @@ const CRYPTO_SETTINGS_LABELS = [
|
||||||
const THEMES = [
|
const THEMES = [
|
||||||
{
|
{
|
||||||
id: 'theme',
|
id: 'theme',
|
||||||
label: 'Light theme',
|
label: _td('Light theme'),
|
||||||
value: 'light',
|
value: 'light',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'theme',
|
id: 'theme',
|
||||||
label: 'Dark theme',
|
label: _td('Dark theme'),
|
||||||
value: 'dark',
|
value: 'dark',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const IgnoredUser = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
userId: React.PropTypes.string.isRequired,
|
||||||
|
onUnignored: React.PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
_onUnignoreClick: function() {
|
||||||
|
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||||
|
const index = ignoredUsers.indexOf(this.props.userId);
|
||||||
|
if (index !== -1) {
|
||||||
|
ignoredUsers.splice(index, 1);
|
||||||
|
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers)
|
||||||
|
.then(() => this.props.onUnignored(this.props.userId));
|
||||||
|
} else this.props.onUnignored(this.props.userId);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<AccessibleButton onClick={this._onUnignoreClick} className="mx_UserSettings_button mx_UserSettings_buttonSmall">
|
||||||
|
{ _t("Unignore") }
|
||||||
|
</AccessibleButton>
|
||||||
|
{ this.props.userId }
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'UserSettings',
|
displayName: 'UserSettings',
|
||||||
|
|
||||||
|
@ -207,6 +239,7 @@ module.exports = React.createClass({
|
||||||
vectorVersion: undefined,
|
vectorVersion: undefined,
|
||||||
rejectingInvites: false,
|
rejectingInvites: false,
|
||||||
mediaDevices: null,
|
mediaDevices: null,
|
||||||
|
ignoredUsers: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -228,6 +261,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
this._refreshMediaDevices();
|
this._refreshMediaDevices();
|
||||||
|
this._refreshIgnoredUsers();
|
||||||
|
|
||||||
// Bulk rejecting invites:
|
// Bulk rejecting invites:
|
||||||
// /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms()
|
// /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms()
|
||||||
|
@ -346,9 +380,22 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_refreshIgnoredUsers: function(userIdUnignored=null) {
|
||||||
|
const users = MatrixClientPeg.get().getIgnoredUsers();
|
||||||
|
if (userIdUnignored) {
|
||||||
|
const index = users.indexOf(userIdUnignored);
|
||||||
|
if (index !== -1) users.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
ignoredUsers: users,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
if (payload.action === "notifier_enabled") {
|
if (payload.action === "notifier_enabled") {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
|
} else if (payload.action === "ignore_state_changed") {
|
||||||
|
this._refreshIgnoredUsers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -627,7 +674,7 @@ module.exports = React.createClass({
|
||||||
<div>
|
<div>
|
||||||
<h3>Referral</h3>
|
<h3>Referral</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{_t("Refer a friend to Riot:")} <a href={href}>{href}</a>
|
{ _t("Refer a friend to Riot:") } <a href={href}>{ href }</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -646,7 +693,7 @@ module.exports = React.createClass({
|
||||||
_renderLanguageSetting: function() {
|
_renderLanguageSetting: function() {
|
||||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||||
return <div>
|
return <div>
|
||||||
<label htmlFor="languageSelector">{_t('Interface Language')}</label>
|
<label htmlFor="languageSelector">{ _t('Interface Language') }</label>
|
||||||
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
|
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
|
||||||
className="mx_UserSettings_language"
|
className="mx_UserSettings_language"
|
||||||
value={this.state.language}
|
value={this.state.language}
|
||||||
|
@ -669,7 +716,7 @@ module.exports = React.createClass({
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{_t('Autocomplete Delay (ms):')}</strong></td>
|
<td><strong>{ _t('Autocomplete Delay (ms):') }</strong></td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -690,8 +737,8 @@ module.exports = React.createClass({
|
||||||
return <div className="mx_UserSettings_toggle">
|
return <div className="mx_UserSettings_toggle">
|
||||||
<input id="urlPreviewsDisabled"
|
<input id="urlPreviewsDisabled"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
defaultChecked={UserSettingsStore.getUrlPreviewsDisabled()}
|
||||||
onChange={ this._onPreviewsDisabledChanged }
|
onChange={this._onPreviewsDisabledChanged}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="urlPreviewsDisabled">
|
<label htmlFor="urlPreviewsDisabled">
|
||||||
{ _t("Disable inline URL previews by default") }
|
{ _t("Disable inline URL previews by default") }
|
||||||
|
@ -712,13 +759,13 @@ module.exports = React.createClass({
|
||||||
if (setting.fn) setting.fn(e.target.checked);
|
if (setting.fn) setting.fn(e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
return <div className="mx_UserSettings_toggle" key={setting.id}>
|
||||||
<input id={ setting.id }
|
<input id={setting.id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ this._syncedSettings[setting.id] }
|
defaultChecked={this._syncedSettings[setting.id]}
|
||||||
onChange={ onChange }
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={ setting.id }>
|
<label htmlFor={setting.id}>
|
||||||
{ _t(setting.label) }
|
{ _t(setting.label) }
|
||||||
</label>
|
</label>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -729,6 +776,7 @@ module.exports = React.createClass({
|
||||||
// to rebind the onChange each time we render
|
// to rebind the onChange each time we render
|
||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
|
this._syncedSettings[setting.id] = setting.value;
|
||||||
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
||||||
}
|
}
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -736,16 +784,16 @@ module.exports = React.createClass({
|
||||||
value: setting.value,
|
value: setting.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
|
return <div className="mx_UserSettings_toggle" key={setting.id + "_" + setting.value}>
|
||||||
<input id={ setting.id + "_" + setting.value }
|
<input id={setting.id + "_" + setting.value}
|
||||||
type="radio"
|
type="radio"
|
||||||
name={ setting.id }
|
name={setting.id}
|
||||||
value={ setting.value }
|
value={setting.value}
|
||||||
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
|
checked={this._syncedSettings[setting.id] === setting.value}
|
||||||
onChange={ onChange }
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={ setting.id + "_" + setting.value }>
|
<label htmlFor={setting.id + "_" + setting.value}>
|
||||||
{ setting.label }
|
{ _t(setting.label) }
|
||||||
</label>
|
</label>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
@ -781,10 +829,10 @@ module.exports = React.createClass({
|
||||||
<h3>{ _t("Cryptography") }</h3>
|
<h3>{ _t("Cryptography") }</h3>
|
||||||
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
|
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
|
||||||
<ul>
|
<ul>
|
||||||
<li><label>{_t("Device ID:")}</label>
|
<li><label>{ _t("Device ID:") }</label>
|
||||||
<span><code>{deviceId}</code></span></li>
|
<span><code>{ deviceId }</code></span></li>
|
||||||
<li><label>{_t("Device key:")}</label>
|
<li><label>{ _t("Device key:") }</label>
|
||||||
<span><code><b>{identityKey}</b></code></span></li>
|
<span><code><b>{ identityKey }</b></code></span></li>
|
||||||
</ul>
|
</ul>
|
||||||
{ importExportButtons }
|
{ importExportButtons }
|
||||||
</div>
|
</div>
|
||||||
|
@ -795,6 +843,26 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderIgnoredUsers: function() {
|
||||||
|
if (this.state.ignoredUsers.length > 0) {
|
||||||
|
const updateHandler = this._refreshIgnoredUsers;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>{ _t("Ignored Users") }</h3>
|
||||||
|
<div className="mx_UserSettings_section mx_UserSettings_ignoredUsersSection">
|
||||||
|
<ul>
|
||||||
|
{ this.state.ignoredUsers.map(function(userId) {
|
||||||
|
return (<IgnoredUser key={userId}
|
||||||
|
userId={userId}
|
||||||
|
onUnignored={updateHandler}></IgnoredUser>);
|
||||||
|
}) }
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else return (<div />);
|
||||||
|
},
|
||||||
|
|
||||||
_renderLocalSetting: function(setting) {
|
_renderLocalSetting: function(setting) {
|
||||||
// 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
|
||||||
|
@ -803,13 +871,13 @@ module.exports = React.createClass({
|
||||||
if (setting.fn) setting.fn(e.target.checked);
|
if (setting.fn) setting.fn(e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
return <div className="mx_UserSettings_toggle" key={setting.id}>
|
||||||
<input id={ setting.id }
|
<input id={setting.id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ this._localSettings[setting.id] }
|
defaultChecked={this._localSettings[setting.id]}
|
||||||
onChange={ onChange }
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={ setting.id }>
|
<label htmlFor={setting.id}>
|
||||||
{ _t(setting.label) }
|
{ _t(setting.label) }
|
||||||
</label>
|
</label>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -819,8 +887,8 @@ module.exports = React.createClass({
|
||||||
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>{_t("Devices")}</h3>
|
<h3>{ _t("Devices") }</h3>
|
||||||
<DevicesPanel className="mx_UserSettings_section"/>
|
<DevicesPanel className="mx_UserSettings_section" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -835,7 +903,7 @@ module.exports = React.createClass({
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<p>{ _t("Found a bug?") }</p>
|
<p>{ _t("Found a bug?") }</p>
|
||||||
<button className="mx_UserSettings_button danger"
|
<button className="mx_UserSettings_button danger"
|
||||||
onClick={this._onBugReportClicked}>{_t('Report it')}
|
onClick={this._onBugReportClicked}>{ _t('Report it') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -843,13 +911,13 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderAnalyticsControl: function() {
|
_renderAnalyticsControl: function() {
|
||||||
if (!SdkConfig.get().piwik) return <div/>;
|
if (!SdkConfig.get().piwik) return <div />;
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<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_LABELS.map( this._renderLocalSetting ) }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
@ -879,10 +947,10 @@ module.exports = React.createClass({
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={feature.id}
|
id={feature.id}
|
||||||
name={feature.id}
|
name={feature.id}
|
||||||
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
|
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)}
|
||||||
onChange={ onChange }
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={feature.id}>{feature.name}</label>
|
<label htmlFor={feature.id}>{ feature.name }</label>
|
||||||
</div>);
|
</div>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -896,7 +964,7 @@ module.exports = React.createClass({
|
||||||
<h3>{ _t("Labs") }</h3>
|
<h3>{ _t("Labs") }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
|
<p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
|
||||||
{features}
|
{ features }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -929,10 +997,10 @@ module.exports = React.createClass({
|
||||||
const platform = PlatformPeg.get();
|
const platform = PlatformPeg.get();
|
||||||
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
|
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
|
||||||
return <div>
|
return <div>
|
||||||
<h3>{_t('Updates')}</h3>
|
<h3>{ _t('Updates') }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
|
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
|
||||||
{_t('Check for update')}
|
{ _t('Check for update') }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -958,7 +1026,7 @@ module.exports = React.createClass({
|
||||||
reject = (
|
reject = (
|
||||||
<AccessibleButton className="mx_UserSettings_button danger"
|
<AccessibleButton className="mx_UserSettings_button danger"
|
||||||
onClick={onClick}>
|
onClick={onClick}>
|
||||||
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
|
{ _t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length}) }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -966,7 +1034,7 @@ module.exports = React.createClass({
|
||||||
return <div>
|
return <div>
|
||||||
<h3>{ _t("Bulk Options") }</h3>
|
<h3>{ _t("Bulk Options") }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{reject}
|
{ reject }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
@ -984,7 +1052,7 @@ module.exports = React.createClass({
|
||||||
defaultChecked={settings['auto-launch']}
|
defaultChecked={settings['auto-launch']}
|
||||||
onChange={this._onAutoLaunchChanged}
|
onChange={this._onAutoLaunchChanged}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="auto-launch">{_t('Start automatically after system login')}</label>
|
<label htmlFor="auto-launch">{ _t('Start automatically after system login') }</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -996,7 +1064,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_mapWebRtcDevicesToSpans: function(devices) {
|
_mapWebRtcDevicesToSpans: function(devices) {
|
||||||
return devices.map((device) => <span key={device.deviceId}>{device.label}</span>);
|
return devices.map((device) => <span key={device.deviceId}>{ device.label }</span>);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setAudioInput: function(deviceId) {
|
_setAudioInput: function(deviceId) {
|
||||||
|
@ -1032,15 +1100,15 @@ module.exports = React.createClass({
|
||||||
if (this.state.mediaDevices === false) {
|
if (this.state.mediaDevices === false) {
|
||||||
return (
|
return (
|
||||||
<p className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
|
<p className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
|
||||||
{_t('Missing Media Permissions, click here to request.')}
|
{ _t('Missing Media Permissions, click here to request.') }
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
} else if (!this.state.mediaDevices) return;
|
} else if (!this.state.mediaDevices) return;
|
||||||
|
|
||||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||||
|
|
||||||
let microphoneDropdown = <p>{_t('No Microphones detected')}</p>;
|
let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
||||||
let webcamDropdown = <p>{_t('No Webcams detected')}</p>;
|
let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
||||||
|
|
||||||
const defaultOption = {
|
const defaultOption = {
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
|
@ -1057,12 +1125,12 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
microphoneDropdown = <div>
|
microphoneDropdown = <div>
|
||||||
<h4>{_t('Microphone')}</h4>
|
<h4>{ _t('Microphone') }</h4>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||||
value={this.state.activeAudioInput || defaultInput}
|
value={this.state.activeAudioInput || defaultInput}
|
||||||
onOptionChange={this._setAudioInput}>
|
onOptionChange={this._setAudioInput}>
|
||||||
{this._mapWebRtcDevicesToSpans(audioInputs)}
|
{ this._mapWebRtcDevicesToSpans(audioInputs) }
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -1077,25 +1145,25 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
webcamDropdown = <div>
|
webcamDropdown = <div>
|
||||||
<h4>{_t('Camera')}</h4>
|
<h4>{ _t('Camera') }</h4>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||||
value={this.state.activeVideoInput || defaultInput}
|
value={this.state.activeVideoInput || defaultInput}
|
||||||
onOptionChange={this._setVideoInput}>
|
onOptionChange={this._setVideoInput}>
|
||||||
{this._mapWebRtcDevicesToSpans(videoInputs)}
|
{ this._mapWebRtcDevicesToSpans(videoInputs) }
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
{microphoneDropdown}
|
{ microphoneDropdown }
|
||||||
{webcamDropdown}
|
{ webcamDropdown }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderWebRtcSettings: function() {
|
_renderWebRtcSettings: function() {
|
||||||
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_LABELS.map(this._renderLocalSetting) }
|
||||||
{ this._renderWebRtcDeviceSettings() }
|
{ this._renderWebRtcDeviceSettings() }
|
||||||
|
@ -1161,7 +1229,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
<label htmlFor={id}>{ this.nameForMedium(val.medium) }</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
<input type="text" key={val.address} id={id}
|
<input type="text" key={val.address} id={id}
|
||||||
|
@ -1169,7 +1237,7 @@ module.exports = React.createClass({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||||
<img src="img/cancel-small.svg" width="14" height="14" alt={ _t("Remove") }
|
<img src="img/cancel-small.svg" width="14" height="14" alt={_t("Remove")}
|
||||||
onClick={onRemoveClick} />
|
onClick={onRemoveClick} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1182,16 +1250,16 @@ module.exports = React.createClass({
|
||||||
addEmailSection = (
|
addEmailSection = (
|
||||||
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
<label>{_t('Email')}</label>
|
<label>{ _t('Email') }</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
<EditableText
|
<EditableText
|
||||||
ref="add_email_input"
|
ref="add_email_input"
|
||||||
className="mx_UserSettings_editable"
|
className="mx_UserSettings_editable"
|
||||||
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
||||||
placeholder={ _t("Add email address") }
|
placeholder={_t("Add email address")}
|
||||||
blurToCancel={ false }
|
blurToCancel={false}
|
||||||
onValueChanged={ this._onAddEmailEditFinished } />
|
onValueChanged={this._onAddEmailEditFinished} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||||
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
||||||
|
@ -1239,8 +1307,8 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserSettings">
|
<div className="mx_UserSettings">
|
||||||
<SimpleRoomHeader
|
<SimpleRoomHeader
|
||||||
title={ _t("Settings") }
|
title={_t("Settings")}
|
||||||
onCancelClick={ this.props.onClose }
|
onCancelClick={this.props.onClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GeminiScrollbar className="mx_UserSettings_body"
|
<GeminiScrollbar className="mx_UserSettings_body"
|
||||||
|
@ -1258,21 +1326,21 @@ module.exports = React.createClass({
|
||||||
<ChangeDisplayName />
|
<ChangeDisplayName />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{threepidsSection}
|
{ threepidsSection }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_UserSettings_avatarPicker">
|
<div className="mx_UserSettings_avatarPicker">
|
||||||
<div onClick={ this.onAvatarPickerClick }>
|
<div onClick={this.onAvatarPickerClick}>
|
||||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
|
showUploadSection={false} className="mx_UserSettings_avatarPicker_img" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_avatarPicker_edit">
|
<div className="mx_UserSettings_avatarPicker_edit">
|
||||||
<label htmlFor="avatarInput" ref="file_label">
|
<label htmlFor="avatarInput" ref="file_label">
|
||||||
<img src="img/camera.svg" className="mx_filterFlipColor"
|
<img src="img/camera.svg" className="mx_filterFlipColor"
|
||||||
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
|
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
||||||
width="17" height="15" />
|
width="17" height="15" />
|
||||||
</label>
|
</label>
|
||||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
|
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1289,36 +1357,37 @@ module.exports = React.createClass({
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
{accountJsx}
|
{ accountJsx }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this._renderReferral()}
|
{ this._renderReferral() }
|
||||||
|
|
||||||
{notificationArea}
|
{ notificationArea }
|
||||||
|
|
||||||
{this._renderUserInterfaceSettings()}
|
{ this._renderUserInterfaceSettings() }
|
||||||
{this._renderLabs()}
|
{ this._renderLabs() }
|
||||||
{this._renderWebRtcSettings()}
|
{ this._renderWebRtcSettings() }
|
||||||
{this._renderDevicesPanel()}
|
{ this._renderDevicesPanel() }
|
||||||
{this._renderCryptoInfo()}
|
{ this._renderCryptoInfo() }
|
||||||
{this._renderBulkOptions()}
|
{ this._renderIgnoredUsers() }
|
||||||
{this._renderBugReport()}
|
{ this._renderBulkOptions() }
|
||||||
|
{ this._renderBugReport() }
|
||||||
|
|
||||||
{PlatformPeg.get().isElectron() && this._renderElectronSettings()}
|
{ PlatformPeg.get().isElectron() && this._renderElectronSettings() }
|
||||||
|
|
||||||
{this._renderAnalyticsControl()}
|
{ this._renderAnalyticsControl() }
|
||||||
|
|
||||||
<h3>{ _t("Advanced") }</h3>
|
<h3>{ _t("Advanced") }</h3>
|
||||||
|
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
{ _t("Logged in as:") } {this._me}
|
{ _t("Logged in as:") } { this._me }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
{_t('Access Token:')}
|
{ _t('Access Token:') }
|
||||||
<span className="mx_UserSettings_advanced_spoiler"
|
<span className="mx_UserSettings_advanced_spoiler"
|
||||||
onClick={this._showSpoiler}
|
onClick={this._showSpoiler}
|
||||||
data-spoiler={ MatrixClientPeg.get().getAccessToken() }>
|
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||||
<{ _t("click to reveal") }>
|
<{ _t("click to reveal") }>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1329,23 +1398,23 @@ module.exports = React.createClass({
|
||||||
{ _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
|
{ _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
{_t('matrix-react-sdk version:')} {(REACT_SDK_VERSION !== '<local>')
|
{ _t('matrix-react-sdk version:') } { (REACT_SDK_VERSION !== '<local>')
|
||||||
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
|
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
|
||||||
: REACT_SDK_VERSION
|
: REACT_SDK_VERSION
|
||||||
}<br/>
|
}<br />
|
||||||
{_t('riot-web version:')} {(this.state.vectorVersion !== undefined)
|
{ _t('riot-web version:') } { (this.state.vectorVersion !== undefined)
|
||||||
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
||||||
: 'unknown'
|
: 'unknown'
|
||||||
}<br/>
|
}<br />
|
||||||
{ _t("olm version:") } {olmVersionString}<br/>
|
{ _t("olm version:") } { olmVersionString }<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this._renderCheckUpdate()}
|
{ this._renderCheckUpdate() }
|
||||||
|
|
||||||
{this._renderClearCache()}
|
{ this._renderClearCache() }
|
||||||
|
|
||||||
{this._renderDeactivateAccount()}
|
{ this._renderDeactivateAccount() }
|
||||||
|
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 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.
|
||||||
|
@ -136,16 +137,15 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onHsUrlChanged: function(newHsUrl) {
|
onServerConfigChange: function(config) {
|
||||||
this.setState({
|
const newState = {};
|
||||||
enteredHomeserverUrl: newHsUrl
|
if (config.hsUrl !== undefined) {
|
||||||
});
|
newState.enteredHomeserverUrl = config.hsUrl;
|
||||||
},
|
}
|
||||||
|
if (config.isUrl !== undefined) {
|
||||||
onIsUrlChanged: function(newIsUrl) {
|
newState.enteredIdentityServerUrl = config.isUrl;
|
||||||
this.setState({
|
}
|
||||||
enteredIdentityServerUrl: newIsUrl
|
this.setState(newState);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showErrorDialog: function(body, title) {
|
showErrorDialog: function(body, title) {
|
||||||
|
@ -170,7 +170,7 @@ module.exports = React.createClass({
|
||||||
else if (this.state.progress === "sent_email") {
|
else if (this.state.progress === "sent_email") {
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
<div>
|
<div>
|
||||||
{ _t('An email has been sent to') } {this.state.email}. { _t('Once you've followed the link it contains, click below') }.
|
{ _t('An email has been sent to') } {this.state.email}. { _t("Once you've followed the link it contains, click below") }.
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||||
value={ _t('I have verified my email address') } />
|
value={ _t('I have verified my email address') } />
|
||||||
|
@ -221,8 +221,7 @@ module.exports = React.createClass({
|
||||||
defaultIsUrl={this.props.defaultIsUrl}
|
defaultIsUrl={this.props.defaultIsUrl}
|
||||||
customHsUrl={this.props.customHsUrl}
|
customHsUrl={this.props.customHsUrl}
|
||||||
customIsUrl={this.props.customIsUrl}
|
customIsUrl={this.props.customIsUrl}
|
||||||
onHsUrlChanged={this.onHsUrlChanged}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
onIsUrlChanged={this.onIsUrlChanged}
|
|
||||||
delayTimeMs={0}/>
|
delayTimeMs={0}/>
|
||||||
<div className="mx_Login_error">
|
<div className="mx_Login_error">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
import React from 'react';
|
||||||
|
import AvatarLogic from '../../../Avatar';
|
||||||
var React = require('react');
|
|
||||||
var AvatarLogic = require("../../../Avatar");
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
|
|
|
@ -13,11 +13,10 @@ 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.
|
||||||
*/
|
*/
|
||||||
var React = require('react');
|
import React from "react";
|
||||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
import {ContentRepo} from "matrix-js-sdk";
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
var Avatar = require('../../../Avatar');
|
import sdk from "../../../index";
|
||||||
var sdk = require("../../../index");
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomAvatar',
|
displayName: 'RoomAvatar',
|
||||||
|
@ -30,7 +29,7 @@ module.exports = React.createClass({
|
||||||
oobData: React.PropTypes.object,
|
oobData: React.PropTypes.object,
|
||||||
width: React.PropTypes.number,
|
width: React.PropTypes.number,
|
||||||
height: React.PropTypes.number,
|
height: React.PropTypes.number,
|
||||||
resizeMethod: React.PropTypes.string
|
resizeMethod: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -44,13 +43,13 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
urls: this.getImageUrls(this.props)
|
urls: this.getImageUrls(this.props),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
this.setState({
|
this.setState({
|
||||||
urls: this.getImageUrls(newProps)
|
urls: this.getImageUrls(newProps),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -61,11 +60,10 @@ module.exports = React.createClass({
|
||||||
props.oobData.avatarUrl,
|
props.oobData.avatarUrl,
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod
|
props.resizeMethod,
|
||||||
), // highest priority
|
), // highest priority
|
||||||
this.getRoomAvatarUrl(props),
|
this.getRoomAvatarUrl(props),
|
||||||
this.getOneToOneAvatar(props),
|
this.getOneToOneAvatar(props), // lowest priority
|
||||||
this.getFallbackAvatar(props) // lowest priority
|
|
||||||
].filter(function(url) {
|
].filter(function(url) {
|
||||||
return (url != null && url != "");
|
return (url != null && url != "");
|
||||||
});
|
});
|
||||||
|
@ -79,17 +77,17 @@ module.exports = React.createClass({
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
getOneToOneAvatar: function(props) {
|
getOneToOneAvatar: function(props) {
|
||||||
if (!props.room) return null;
|
if (!props.room) return null;
|
||||||
|
|
||||||
var mlist = props.room.currentState.members;
|
const mlist = props.room.currentState.members;
|
||||||
var userIds = [];
|
const userIds = [];
|
||||||
// for .. in optimisation to return early if there are >2 keys
|
// for .. in optimisation to return early if there are >2 keys
|
||||||
for (var uid in mlist) {
|
for (const uid in mlist) {
|
||||||
if (mlist.hasOwnProperty(uid)) {
|
if (mlist.hasOwnProperty(uid)) {
|
||||||
userIds.push(uid);
|
userIds.push(uid);
|
||||||
}
|
}
|
||||||
|
@ -99,7 +97,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userIds.length == 2) {
|
if (userIds.length == 2) {
|
||||||
var theOtherGuy = null;
|
let theOtherGuy = null;
|
||||||
if (mlist[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
if (mlist[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
theOtherGuy = mlist[userIds[1]];
|
theOtherGuy = mlist[userIds[1]];
|
||||||
} else {
|
} else {
|
||||||
|
@ -110,7 +108,7 @@ module.exports = React.createClass({
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
} else if (userIds.length == 1) {
|
} else if (userIds.length == 1) {
|
||||||
return mlist[userIds[0]].getAvatarUrl(
|
return mlist[userIds[0]].getAvatarUrl(
|
||||||
|
@ -118,37 +116,24 @@ module.exports = React.createClass({
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getFallbackAvatar: function(props) {
|
|
||||||
let roomId = null;
|
|
||||||
if (props.oobData && props.oobData.roomId) {
|
|
||||||
roomId = this.props.oobData.roomId;
|
|
||||||
} else if (props.room) {
|
|
||||||
roomId = props.room.roomId;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Avatar.defaultAvatarUrlForString(roomId);
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
|
|
||||||
var {room, oobData, ...otherProps} = this.props;
|
const {room, oobData, ...otherProps} = this.props;
|
||||||
|
|
||||||
var roomName = room ? room.name : oobData.name;
|
const roomName = room ? room.name : oobData.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar {...otherProps} name={roomName}
|
<BaseAvatar {...otherProps} name={roomName}
|
||||||
idName={room ? room.roomId : null}
|
idName={room ? room.roomId : null}
|
||||||
urls={this.state.urls} />
|
urls={this.state.urls} />
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,7 +28,7 @@ const TRUNCATE_QUERY_LIST = 40;
|
||||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: "UserPickerDialog",
|
displayName: "AddressPickerDialog",
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
|
@ -38,8 +38,14 @@ module.exports = React.createClass({
|
||||||
roomId: PropTypes.string,
|
roomId: PropTypes.string,
|
||||||
button: PropTypes.string,
|
button: PropTypes.string,
|
||||||
focus: PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOfType(addressTypes)),
|
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
groupId: PropTypes.string,
|
||||||
|
// The type of entity to search for. Default: 'user'.
|
||||||
|
pickerType: PropTypes.oneOf(['user', 'room']),
|
||||||
|
// Whether the current user should be included in the addresses returned. Only
|
||||||
|
// applicable when pickerType is `user`. Default: false.
|
||||||
|
includeSelf: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -47,6 +53,8 @@ module.exports = React.createClass({
|
||||||
value: "",
|
value: "",
|
||||||
focus: true,
|
focus: true,
|
||||||
validAddressTypes: addressTypes,
|
validAddressTypes: addressTypes,
|
||||||
|
pickerType: 'user',
|
||||||
|
includeSelf: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -140,10 +148,22 @@ module.exports = React.createClass({
|
||||||
// Only do search if there is something to search
|
// Only do search if there is something to search
|
||||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||||
this.queryChangedDebouncer = setTimeout(() => {
|
this.queryChangedDebouncer = setTimeout(() => {
|
||||||
if (this.state.serverSupportsUserDirectory) {
|
if (this.props.pickerType === 'user') {
|
||||||
this._doUserDirectorySearch(query);
|
if (this.props.groupId) {
|
||||||
|
this._doNaiveGroupSearch(query);
|
||||||
|
} else if (this.state.serverSupportsUserDirectory) {
|
||||||
|
this._doUserDirectorySearch(query);
|
||||||
|
} else {
|
||||||
|
this._doLocalSearch(query);
|
||||||
|
}
|
||||||
|
} else if (this.props.pickerType === 'room') {
|
||||||
|
if (this.props.groupId) {
|
||||||
|
this._doNaiveGroupRoomSearch(query);
|
||||||
|
} else {
|
||||||
|
this._doRoomSearch(query);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this._doLocalSearch(query);
|
console.error('Unknown pickerType', this.props.pickerType);
|
||||||
}
|
}
|
||||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||||
} else {
|
} else {
|
||||||
|
@ -185,6 +205,101 @@ module.exports = React.createClass({
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_doNaiveGroupSearch: function(query) {
|
||||||
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
|
this.setState({
|
||||||
|
busy: true,
|
||||||
|
query,
|
||||||
|
searchError: null,
|
||||||
|
});
|
||||||
|
MatrixClientPeg.get().getGroupUsers(this.props.groupId).then((resp) => {
|
||||||
|
const results = [];
|
||||||
|
resp.chunk.forEach((u) => {
|
||||||
|
const userIdMatch = u.user_id.toLowerCase().includes(lowerCaseQuery);
|
||||||
|
const displayNameMatch = (u.displayname || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
if (!(userIdMatch || displayNameMatch)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
results.push({
|
||||||
|
user_id: u.user_id,
|
||||||
|
avatar_url: u.avatar_url,
|
||||||
|
display_name: u.displayname,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._processResults(results, query);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('Error whilst searching group rooms: ', err);
|
||||||
|
this.setState({
|
||||||
|
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||||
|
});
|
||||||
|
}).done(() => {
|
||||||
|
this.setState({
|
||||||
|
busy: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_doNaiveGroupRoomSearch: function(query) {
|
||||||
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
|
MatrixClientPeg.get().getGroupRooms(this.props.groupId).then((resp) => {
|
||||||
|
const results = [];
|
||||||
|
resp.chunk.forEach((r) => {
|
||||||
|
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
if (!(nameMatch || topicMatch || aliasMatch)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
results.push({
|
||||||
|
room_id: r.room_id,
|
||||||
|
avatar_url: r.avatar_url,
|
||||||
|
name: r.name || r.canonical_alias,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._processResults(results, query);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('Error whilst searching group users: ', err);
|
||||||
|
this.setState({
|
||||||
|
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||||
|
});
|
||||||
|
}).done(() => {
|
||||||
|
this.setState({
|
||||||
|
busy: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_doRoomSearch: function(query) {
|
||||||
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
|
const rooms = MatrixClientPeg.get().getRooms();
|
||||||
|
const results = [];
|
||||||
|
rooms.forEach((room) => {
|
||||||
|
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
|
||||||
|
const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
|
||||||
|
const name = nameEvent ? nameEvent.getContent().name : '';
|
||||||
|
const canonicalAlias = room.getCanonicalAlias();
|
||||||
|
const topic = topicEvent ? topicEvent.getContent().topic : '';
|
||||||
|
|
||||||
|
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
const aliasMatch = (canonicalAlias || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||||
|
if (!(nameMatch || topicMatch || aliasMatch)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
|
||||||
|
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
|
||||||
|
results.push({
|
||||||
|
room_id: room.roomId,
|
||||||
|
avatar_url: avatarUrl,
|
||||||
|
name: name || canonicalAlias,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._processResults(results, query);
|
||||||
|
this.setState({
|
||||||
|
busy: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_doUserDirectorySearch: function(query) {
|
_doUserDirectorySearch: function(query) {
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -245,17 +360,30 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_processResults: function(results, query) {
|
_processResults: function(results, query) {
|
||||||
const queryList = [];
|
const queryList = [];
|
||||||
results.forEach((user) => {
|
results.forEach((result) => {
|
||||||
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
|
if (result.room_id) {
|
||||||
|
queryList.push({
|
||||||
|
addressType: 'mx-room-id',
|
||||||
|
address: result.room_id,
|
||||||
|
displayName: result.name,
|
||||||
|
avatarMxc: result.avatar_url,
|
||||||
|
isKnown: true,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this.props.includeSelf &&
|
||||||
|
result.user_id === MatrixClientPeg.get().credentials.userId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Return objects, structure of which is defined
|
// Return objects, structure of which is defined
|
||||||
// by UserAddressType
|
// by UserAddressType
|
||||||
queryList.push({
|
queryList.push({
|
||||||
addressType: 'mx',
|
addressType: 'mx-user-id',
|
||||||
address: user.user_id,
|
address: result.user_id,
|
||||||
displayName: user.display_name,
|
displayName: result.display_name,
|
||||||
avatarMxc: user.avatar_url,
|
avatarMxc: result.avatar_url,
|
||||||
isKnown: true,
|
isKnown: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -291,16 +419,23 @@ module.exports = React.createClass({
|
||||||
address: addressText,
|
address: addressText,
|
||||||
isKnown: false,
|
isKnown: false,
|
||||||
};
|
};
|
||||||
if (addrType == null) {
|
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||||
this.setState({ error: true });
|
this.setState({ error: true });
|
||||||
return null;
|
return null;
|
||||||
} else if (addrType == 'mx') {
|
} else if (addrType == 'mx-user-id') {
|
||||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||||
if (user) {
|
if (user) {
|
||||||
addrObj.displayName = user.displayName;
|
addrObj.displayName = user.displayName;
|
||||||
addrObj.avatarMxc = user.avatarUrl;
|
addrObj.avatarMxc = user.avatarUrl;
|
||||||
addrObj.isKnown = true;
|
addrObj.isKnown = true;
|
||||||
}
|
}
|
||||||
|
} else if (addrType == 'mx-room-id') {
|
||||||
|
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||||
|
if (room) {
|
||||||
|
addrObj.displayName = room.name;
|
||||||
|
addrObj.avatarMxc = room.avatarUrl;
|
||||||
|
addrObj.isKnown = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userList = this.state.userList.slice();
|
const userList = this.state.userList.slice();
|
||||||
|
@ -360,7 +495,7 @@ module.exports = React.createClass({
|
||||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||||
for (let i = 0; i < this.state.userList.length; i++) {
|
for (let i = 0; i < this.state.userList.length; i++) {
|
||||||
query.push(
|
query.push(
|
||||||
<AddressTile key={i} address={this.state.userList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />,
|
<AddressTile key={i} address={this.state.userList[i]} canDismiss={true} onDismissed={this.onDismissed(i)} />,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -382,23 +517,36 @@ module.exports = React.createClass({
|
||||||
let error;
|
let error;
|
||||||
let addressSelector;
|
let addressSelector;
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
|
let tryUsing = '';
|
||||||
|
const validTypeDescriptions = this.props.validAddressTypes.map((t) => {
|
||||||
|
return {
|
||||||
|
'mx-user-id': _t("Matrix ID"),
|
||||||
|
'mx-room-id': _t("Matrix Room ID"),
|
||||||
|
'email': _t("email address"),
|
||||||
|
}[t];
|
||||||
|
});
|
||||||
|
tryUsing = _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||||
|
validTypesList: validTypeDescriptions.join(", "),
|
||||||
|
});
|
||||||
error = <div className="mx_ChatInviteDialog_error">
|
error = <div className="mx_ChatInviteDialog_error">
|
||||||
{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}
|
{ _t("You have entered an invalid address.") }
|
||||||
|
<br />
|
||||||
|
{ tryUsing }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.searchError) {
|
} else if (this.state.searchError) {
|
||||||
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
|
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
|
||||||
} else if (
|
} else if (
|
||||||
this.state.query.length > 0 &&
|
this.state.query.length > 0 &&
|
||||||
this.state.queryList.length === 0 &&
|
this.state.queryList.length === 0 &&
|
||||||
!this.state.busy
|
!this.state.busy
|
||||||
) {
|
) {
|
||||||
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
|
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
||||||
} else {
|
} else {
|
||||||
addressSelector = (
|
addressSelector = (
|
||||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||||
addressList={ this.state.queryList }
|
addressList={this.state.queryList}
|
||||||
onSelected={ this.onSelected }
|
onSelected={this.onSelected}
|
||||||
truncateAt={ TRUNCATE_QUERY_LIST }
|
truncateAt={TRUNCATE_QUERY_LIST}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -406,7 +554,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
||||||
<div className="mx_Dialog_title">
|
<div className="mx_Dialog_title">
|
||||||
{this.props.title}
|
{ this.props.title }
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
||||||
onClick={this.onCancel} >
|
onClick={this.onCancel} >
|
||||||
|
@ -422,7 +570,7 @@ module.exports = React.createClass({
|
||||||
</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}>
|
||||||
{this.props.button}
|
{ this.props.button }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -52,20 +52,20 @@ export default React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={ this.onOk }
|
onEnterPressed={this.onOk}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{_t("Are you sure you wish to remove (delete) this event? " +
|
{ _t("Are you sure you wish to remove (delete) this event? " +
|
||||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
"Note that if you delete a room name or topic change, it could undo the change.") }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className={confirmButtonClass} onClick={this.onOk}>
|
<button className={confirmButtonClass} onClick={this.onOk}>
|
||||||
{_t("Remove")}
|
{ _t("Remove") }
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onClick={this.onCancel}>
|
<button onClick={this.onCancel}>
|
||||||
{_t("Cancel")}
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { GroupMemberType } from '../../../groups';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A dialog for confirming an operation on another user.
|
* A dialog for confirming an operation on another user.
|
||||||
|
@ -30,7 +31,10 @@ import classnames from 'classnames';
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'ConfirmUserActionDialog',
|
displayName: 'ConfirmUserActionDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
member: React.PropTypes.object.isRequired, // matrix-js-sdk member object
|
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||||
|
member: React.PropTypes.object,
|
||||||
|
// group member object. Supply either this or 'member'
|
||||||
|
groupMember: GroupMemberType,
|
||||||
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
||||||
|
|
||||||
// Whether to display a text field for a reason
|
// Whether to display a text field for a reason
|
||||||
|
@ -69,6 +73,7 @@ export default React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||||
|
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||||
|
|
||||||
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
|
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
|
||||||
const confirmButtonClass = classnames({
|
const confirmButtonClass = classnames({
|
||||||
|
@ -83,7 +88,7 @@ export default React.createClass({
|
||||||
<form onSubmit={this.onOk}>
|
<form onSubmit={this.onOk}>
|
||||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
<input className="mx_ConfirmUserActionDialog_reasonField"
|
||||||
ref={this._collectReasonField}
|
ref={this._collectReasonField}
|
||||||
placeholder={ _t("Reason") }
|
placeholder={_t("Reason")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
@ -91,24 +96,38 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let avatar;
|
||||||
|
let name;
|
||||||
|
let userId;
|
||||||
|
if (this.props.member) {
|
||||||
|
avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
|
||||||
|
name = this.props.member.name;
|
||||||
|
userId = this.props.member.userId;
|
||||||
|
} else {
|
||||||
|
// we don't get this info from the API yet
|
||||||
|
avatar = <BaseAvatar name={this.props.groupMember.userId} width={48} height={48} />;
|
||||||
|
name = this.props.groupMember.userId;
|
||||||
|
userId = this.props.groupMember.userId;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={ this.onOk }
|
onEnterPressed={this.onOk}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
{ avatar }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
|
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
|
||||||
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
|
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||||
</div>
|
</div>
|
||||||
{reasonBox}
|
{ reasonBox }
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className={confirmButtonClass}
|
<button className={confirmButtonClass}
|
||||||
onClick={this.onOk} autoFocus={!this.props.askReason}
|
onClick={this.onOk} autoFocus={!this.props.askReason}
|
||||||
>
|
>
|
||||||
{this.props.action}
|
{ this.props.action }
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onClick={this.onCancel}>
|
<button onClick={this.onCancel}>
|
||||||
|
|
|
@ -142,8 +142,8 @@ export default React.createClass({
|
||||||
// rather than displaying what the server gives us, but synapse doesn't give
|
// rather than displaying what the server gives us, but synapse doesn't give
|
||||||
// any yet.
|
// any yet.
|
||||||
createErrorNode = <div className="error">
|
createErrorNode = <div className="error">
|
||||||
<div>{_t('Room creation failed')}</div>
|
<div>{ _t('Room creation failed') }</div>
|
||||||
<div>{this.state.createError.message}</div>
|
<div>{ this.state.createError.message }</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@ export default React.createClass({
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_CreateGroupDialog_inputRow">
|
<div className="mx_CreateGroupDialog_inputRow">
|
||||||
<div className="mx_CreateGroupDialog_label">
|
<div className="mx_CreateGroupDialog_label">
|
||||||
<label htmlFor="groupname">{_t('Group Name')}</label>
|
<label htmlFor="groupname">{ _t('Group Name') }</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||||
|
@ -169,7 +169,7 @@ export default React.createClass({
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CreateGroupDialog_inputRow">
|
<div className="mx_CreateGroupDialog_inputRow">
|
||||||
<div className="mx_CreateGroupDialog_label">
|
<div className="mx_CreateGroupDialog_label">
|
||||||
<label htmlFor="groupid">{_t('Group ID')}</label>
|
<label htmlFor="groupid">{ _t('Group ID') }</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="groupid" className="mx_CreateGroupDialog_input"
|
<input id="groupid" className="mx_CreateGroupDialog_input"
|
||||||
|
@ -182,9 +182,9 @@ export default React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="error">
|
<div className="error">
|
||||||
{this.state.groupIdError}
|
{ this.state.groupIdError }
|
||||||
</div>
|
</div>
|
||||||
{createErrorNode}
|
{ createErrorNode }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this._onCancel}>
|
<button onClick={this._onCancel}>
|
||||||
|
|
|
@ -28,25 +28,25 @@ export default function DeviceVerifyDialog(props) {
|
||||||
const body = (
|
const body = (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{_t("To verify that this device can be trusted, please contact its " +
|
{ _t("To verify that this device can be trusted, please contact its " +
|
||||||
"owner using some other means (e.g. in person or a phone call) " +
|
"owner using some other means (e.g. in person or a phone call) " +
|
||||||
"and ask them whether the key they see in their User Settings " +
|
"and ask them whether the key they see in their User Settings " +
|
||||||
"for this device matches the key below:")}
|
"for this device matches the key below:") }
|
||||||
</p>
|
</p>
|
||||||
<div className="mx_UserSettings_cryptoSection">
|
<div className="mx_UserSettings_cryptoSection">
|
||||||
<ul>
|
<ul>
|
||||||
<li><label>{_t("Device name")}:</label> <span>{ props.device.getDisplayName() }</span></li>
|
<li><label>{ _t("Device name") }:</label> <span>{ props.device.getDisplayName() }</span></li>
|
||||||
<li><label>{_t("Device ID")}:</label> <span><code>{ props.device.deviceId}</code></span></li>
|
<li><label>{ _t("Device ID") }:</label> <span><code>{ props.device.deviceId }</code></span></li>
|
||||||
<li><label>{_t("Device key")}:</label> <span><code><b>{ key }</b></code></span></li>
|
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{_t("If it matches, press the verify button below. " +
|
{ _t("If it matches, press the verify button below. " +
|
||||||
"If it doesn't, then someone else is intercepting this device " +
|
"If it doesn't, then someone else is intercepting this device " +
|
||||||
"and you probably want to press the blacklist button instead.")}
|
"and you probably want to press the blacklist button instead.") }
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{_t("In future this verification process will be more sophisticated.")}
|
{ _t("In future this verification process will be more sophisticated.") }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,11 +63,11 @@ export default React.createClass({
|
||||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||||
title={this.props.title || _t('Error')}>
|
title={this.props.title || _t('Error')}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{this.props.description || _t('An error has occurred.')}
|
{ this.props.description || _t('An error has occurred.') }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
||||||
{this.props.button || _t('OK')}
|
{ this.props.button || _t('OK') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -18,7 +18,7 @@ import Modal from '../../../Modal';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog which asks the user whether they want to share their keys with
|
* Dialog which asks the user whether they want to share their keys with
|
||||||
|
@ -116,27 +116,27 @@ export default React.createClass({
|
||||||
|
|
||||||
let text;
|
let text;
|
||||||
if (this.state.wasNewDevice) {
|
if (this.state.wasNewDevice) {
|
||||||
text = "You added a new device '%(displayName)s', which is"
|
text = _td("You added a new device '%(displayName)s', which is"
|
||||||
+ " requesting encryption keys.";
|
+ " requesting encryption keys.");
|
||||||
} else {
|
} else {
|
||||||
text = "Your unverified device '%(displayName)s' is requesting"
|
text = _td("Your unverified device '%(displayName)s' is requesting"
|
||||||
+ " encryption keys.";
|
+ " encryption keys.");
|
||||||
}
|
}
|
||||||
text = _t(text, {displayName: displayName});
|
text = _t(text, {displayName: displayName});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{text}</p>
|
<p>{ text }</p>
|
||||||
|
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this._onVerifyClicked}>
|
<button onClick={this._onVerifyClicked}>
|
||||||
{_t('Start verification')}
|
{ _t('Start verification') }
|
||||||
</button>
|
</button>
|
||||||
<button onClick={this._onShareClicked}>
|
<button onClick={this._onShareClicked}>
|
||||||
{_t('Share without verifying')}
|
{ _t('Share without verifying') }
|
||||||
</button>
|
</button>
|
||||||
<button onClick={this._onIgnoreClicked}>
|
<button onClick={this._onIgnoreClicked}>
|
||||||
{_t('Ignore request')}
|
{ _t('Ignore request') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,7 +154,7 @@ export default React.createClass({
|
||||||
} else {
|
} else {
|
||||||
content = (
|
content = (
|
||||||
<div>
|
<div>
|
||||||
<p>{_t('Loading device info...')}</p>
|
<p>{ _t('Loading device info...') }</p>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -165,7 +165,7 @@ export default React.createClass({
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t('Encryption key request')}
|
title={_t('Encryption key request')}
|
||||||
>
|
>
|
||||||
{content}
|
{ content }
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 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.
|
||||||
|
@ -17,6 +18,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'QuestionDialog',
|
displayName: 'QuestionDialog',
|
||||||
|
@ -25,6 +27,7 @@ export default React.createClass({
|
||||||
description: React.PropTypes.node,
|
description: React.PropTypes.node,
|
||||||
extraButtons: React.PropTypes.node,
|
extraButtons: React.PropTypes.node,
|
||||||
button: React.PropTypes.string,
|
button: React.PropTypes.string,
|
||||||
|
danger: React.PropTypes.bool,
|
||||||
focus: React.PropTypes.bool,
|
focus: React.PropTypes.bool,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
@ -36,6 +39,7 @@ export default React.createClass({
|
||||||
extraButtons: null,
|
extraButtons: null,
|
||||||
focus: true,
|
focus: true,
|
||||||
hasCancelButton: true,
|
hasCancelButton: true,
|
||||||
|
danger: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -51,23 +55,27 @@ export default React.createClass({
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const cancelButton = this.props.hasCancelButton ? (
|
const cancelButton = this.props.hasCancelButton ? (
|
||||||
<button onClick={this.onCancel}>
|
<button onClick={this.onCancel}>
|
||||||
{_t("Cancel")}
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
) : null;
|
) : null;
|
||||||
|
const buttonClasses = classnames({
|
||||||
|
mx_Dialog_primary: true,
|
||||||
|
danger: this.props.danger,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={ this.onOk }
|
onEnterPressed={this.onOk}
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{this.props.description}
|
{ this.props.description }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
|
||||||
{this.props.button || _t('OK')}
|
{ this.props.button || _t('OK') }
|
||||||
</button>
|
</button>
|
||||||
{this.props.extraButtons}
|
{ this.props.extraButtons }
|
||||||
{cancelButton}
|
{ cancelButton }
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,10 +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(
|
{ _tJsx(
|
||||||
"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>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a>,
|
||||||
)}
|
) }
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -57,19 +57,19 @@ export default React.createClass({
|
||||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||||
title={_t('Unable to restore session')}>
|
title={_t('Unable to restore session')}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<p>{_t("We encountered an error trying to restore your previous session. If " +
|
<p>{ _t("We encountered an error trying to restore your previous session. If " +
|
||||||
"you continue, you will need to log in again, and encrypted chat " +
|
"you continue, you will need to log in again, and encrypted chat " +
|
||||||
"history will be unreadable.")}</p>
|
"history will be unreadable.") }</p>
|
||||||
|
|
||||||
<p>{_t("If you have previously used a more recent version of Riot, your session " +
|
<p>{ _t("If you have previously used a more recent version of Riot, your session " +
|
||||||
"may be incompatible with this version. Close this window and return " +
|
"may be incompatible with this version. Close this window and return " +
|
||||||
"to the more recent version.")}</p>
|
"to the more recent version.") }</p>
|
||||||
|
|
||||||
{bugreport}
|
{ bugreport }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
||||||
{_t("Continue anyway")}
|
{ _t("Continue anyway") }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -130,10 +130,10 @@ export default React.createClass({
|
||||||
|
|
||||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||||
className="mx_SetEmailDialog_email_input"
|
className="mx_SetEmailDialog_email_input"
|
||||||
placeholder={ _t("Email address") }
|
placeholder={_t("Email address")}
|
||||||
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
||||||
blurToCancel={ false }
|
blurToCancel={false}
|
||||||
onValueChanged={ this.onEmailAddressChanged } />;
|
onValueChanged={this.onEmailAddressChanged} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_SetEmailDialog"
|
<BaseDialog className="mx_SetEmailDialog"
|
||||||
|
|
|
@ -226,7 +226,7 @@ export default React.createClass({
|
||||||
let usernameIndicator = null;
|
let usernameIndicator = null;
|
||||||
let usernameBusyIndicator = null;
|
let usernameBusyIndicator = null;
|
||||||
if (this.state.usernameBusy) {
|
if (this.state.usernameBusy) {
|
||||||
usernameBusyIndicator = <Spinner w="24" h="24"/>;
|
usernameBusyIndicator = <Spinner w="24" h="24" />;
|
||||||
} else {
|
} else {
|
||||||
const usernameAvailable = this.state.username &&
|
const usernameAvailable = this.state.username &&
|
||||||
this.state.usernameCheckSupport && !this.state.usernameError;
|
this.state.usernameCheckSupport && !this.state.usernameError;
|
||||||
|
@ -275,17 +275,17 @@ export default React.createClass({
|
||||||
/<a>(.*?)<\/a>/,
|
/<a>(.*?)<\/a>/,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
(sub) => <span>{this.props.homeserverUrl}</span>,
|
(sub) => <span>{ this.props.homeserverUrl }</span>,
|
||||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
|
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||||
],
|
],
|
||||||
)}
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{ _tJsx(
|
{ _tJsx(
|
||||||
'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>/,
|
/<a>(.*?)<\/a>/,
|
||||||
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
|
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a>],
|
||||||
)}
|
) }
|
||||||
</p>
|
</p>
|
||||||
{ auth }
|
{ auth }
|
||||||
{ authErrorIndicator }
|
{ authErrorIndicator }
|
||||||
|
|
|
@ -65,10 +65,10 @@ export default React.createClass({
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_TextInputDialog_label">
|
<div className="mx_TextInputDialog_label">
|
||||||
<label htmlFor="textinput"> {this.props.description} </label>
|
<label htmlFor="textinput"> { this.props.description } </label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" onKeyDown={this.onKeyDown}/>
|
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" onKeyDown={this.onKeyDown} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
|
@ -76,7 +76,7 @@ export default React.createClass({
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||||
{this.props.button}
|
{ this.props.button }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import Analytics from '../../../Analytics';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'RoleButton',
|
displayName: 'RoleButton',
|
||||||
|
@ -47,6 +48,7 @@ export default React.createClass({
|
||||||
|
|
||||||
_onClick: function(ev) {
|
_onClick: function(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
Analytics.trackEvent('Action Button', 'click', this.props.action);
|
||||||
dis.dispatch({action: this.props.action});
|
dis.dispatch({action: this.props.action});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -45,11 +45,12 @@ export default React.createClass({
|
||||||
const address = this.props.address;
|
const address = this.props.address;
|
||||||
const name = address.displayName || address.address;
|
const name = address.displayName || address.address;
|
||||||
|
|
||||||
let imgUrls = [];
|
const imgUrls = [];
|
||||||
|
const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType);
|
||||||
|
|
||||||
if (address.addressType === "mx" && address.avatarMxc) {
|
if (isMatrixAddress && address.avatarMxc) {
|
||||||
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
address.avatarMxc, 25, 25, 'crop'
|
address.avatarMxc, 25, 25, 'crop',
|
||||||
));
|
));
|
||||||
} else if (address.addressType === 'email') {
|
} else if (address.addressType === 'email') {
|
||||||
imgUrls.push('img/icon-email-user.svg');
|
imgUrls.push('img/icon-email-user.svg');
|
||||||
|
@ -77,7 +78,7 @@ export default React.createClass({
|
||||||
|
|
||||||
let info;
|
let info;
|
||||||
let error = false;
|
let error = false;
|
||||||
if (address.addressType === "mx" && address.isKnown) {
|
if (isMatrixAddress && address.isKnown) {
|
||||||
const idClasses = classNames({
|
const idClasses = classNames({
|
||||||
"mx_AddressTile_id": true,
|
"mx_AddressTile_id": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
@ -89,7 +90,7 @@ export default React.createClass({
|
||||||
<div className={idClasses}>{ address.address }</div>
|
<div className={idClasses}>{ address.address }</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (address.addressType === "mx") {
|
} else if (isMatrixAddress) {
|
||||||
const unknownMxClasses = classNames({
|
const unknownMxClasses = classNames({
|
||||||
"mx_AddressTile_unknownMx": true,
|
"mx_AddressTile_unknownMx": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
|
|
@ -47,13 +47,19 @@ export default class AppPermission extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let e2eWarningText;
|
||||||
|
if (this.props.isRoomEncrypted) {
|
||||||
|
e2eWarningText =
|
||||||
|
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className='mx_AppPermissionWarning'>
|
<div className='mx_AppPermissionWarning'>
|
||||||
<div className='mx_AppPermissionWarningImage'>
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_AppPermissionWarningText'>
|
<div className='mx_AppPermissionWarningText'>
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>Do you want to load widget from URL:</span> <span className='mx_AppPermissionWarningTextURL'>{this.state.curlBase}</span>
|
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
|
||||||
|
{ e2eWarningText }
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
className='mx_AppPermissionButton'
|
className='mx_AppPermissionButton'
|
||||||
|
@ -67,9 +73,11 @@ export default class AppPermission extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
AppPermission.propTypes = {
|
AppPermission.propTypes = {
|
||||||
|
isRoomEncrypted: PropTypes.bool,
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
onPermissionGranted: PropTypes.func.isRequired,
|
onPermissionGranted: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
AppPermission.defaultProps = {
|
AppPermission.defaultProps = {
|
||||||
|
isRoomEncrypted: false,
|
||||||
onPermissionGranted: function() {},
|
onPermissionGranted: function() {},
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,18 +19,19 @@ limitations under the License.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import AppPermission from './AppPermission';
|
import AppPermission from './AppPermission';
|
||||||
import AppWarning from './AppWarning';
|
import AppWarning from './AppWarning';
|
||||||
import MessageSpinner from './MessageSpinner';
|
import MessageSpinner from './MessageSpinner';
|
||||||
import WidgetUtils from '../../../WidgetUtils';
|
import WidgetUtils from '../../../WidgetUtils';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||||
const betaHelpMsg = 'This feature is currently experimental and is intended for beta testing only';
|
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'AppTile',
|
displayName: 'AppTile',
|
||||||
|
@ -44,6 +45,10 @@ export default React.createClass({
|
||||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||||
fullWidth: React.PropTypes.bool,
|
fullWidth: React.PropTypes.bool,
|
||||||
|
// UserId of the current user
|
||||||
|
userId: React.PropTypes.string.isRequired,
|
||||||
|
// UserId of the entity that added / modified the widget
|
||||||
|
creatorUserId: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -59,7 +64,8 @@ export default React.createClass({
|
||||||
loading: false,
|
loading: false,
|
||||||
widgetUrl: this.props.url,
|
widgetUrl: this.props.url,
|
||||||
widgetPermissionId: widgetPermissionId,
|
widgetPermissionId: widgetPermissionId,
|
||||||
hasPermissionToLoad: Boolean(hasPermissionToLoad === 'true'),
|
// Assume that widget has permission to load if we are the user who added it to the room, or if explicitly granted by the user
|
||||||
|
hasPermissionToLoad: hasPermissionToLoad === 'true' || this.props.userId === this.props.creatorUserId,
|
||||||
error: null,
|
error: null,
|
||||||
deleting: false,
|
deleting: false,
|
||||||
};
|
};
|
||||||
|
@ -67,8 +73,17 @@ export default React.createClass({
|
||||||
|
|
||||||
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
|
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
|
||||||
isScalarUrl: function() {
|
isScalarUrl: function() {
|
||||||
const scalarUrl = SdkConfig.get().integrations_rest_url;
|
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
|
||||||
return scalarUrl && this.props.url.startsWith(scalarUrl);
|
if (!scalarUrls || scalarUrls.length == 0) {
|
||||||
|
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < scalarUrls.length; i++) {
|
||||||
|
if (this.props.url.startsWith(scalarUrls[i])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
isMixedContent: function() {
|
isMixedContent: function() {
|
||||||
|
@ -113,6 +128,30 @@ export default React.createClass({
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
window.addEventListener('message', this._onMessage, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('message', this._onMessage);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onMessage(event) {
|
||||||
|
if (this.props.type !== 'jitsi') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!event.origin) {
|
||||||
|
event.origin = event.originalEvent.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.widgetUrl.startsWith(event.origin)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.widgetAction === 'jitsi_iframe_loaded') {
|
||||||
|
const iframe = this.refs.appFrame.contentWindow
|
||||||
|
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
||||||
|
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_canUserModify: function() {
|
_canUserModify: function() {
|
||||||
|
@ -122,7 +161,8 @@ export default React.createClass({
|
||||||
_onEditClick: function(e) {
|
_onEditClick: function(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(this.props.room.roomId, 'type_' + this.props.type);
|
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||||
|
this.props.room.roomId, 'type_' + this.props.type, this.props.id);
|
||||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
src: src,
|
src: src,
|
||||||
}, "mx_IntegrationsManager");
|
}, "mx_IntegrationsManager");
|
||||||
|
@ -155,9 +195,9 @@ export default React.createClass({
|
||||||
// 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() {
|
||||||
if (this._canUserModify()) {
|
if (this._canUserModify()) {
|
||||||
return 'Delete widget';
|
return _td('Delete widget');
|
||||||
}
|
}
|
||||||
return 'Revoke widget access';
|
return _td('Revoke widget access');
|
||||||
},
|
},
|
||||||
|
|
||||||
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
||||||
|
@ -177,11 +217,25 @@ export default React.createClass({
|
||||||
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();
|
||||||
appTileName = appTileName[0].toUpperCase() + appTileName.slice(1).toLowerCase();
|
|
||||||
}
|
}
|
||||||
return appTileName;
|
return appTileName;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onClickMenuBar: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
// Ignore clicks on menu bar children
|
||||||
|
if (ev.target !== this.refs.menu_bar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the view state of the apps drawer
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'appsDrawer',
|
||||||
|
show: !this.props.show,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
|
||||||
|
@ -203,42 +257,46 @@ export default React.createClass({
|
||||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.loading) {
|
if (this.props.show) {
|
||||||
appTileBody = (
|
if (this.state.loading) {
|
||||||
<div className='mx_AppTileBody mx_AppLoading'>
|
appTileBody = (
|
||||||
<MessageSpinner msg='Loading...'/>
|
<div className='mx_AppTileBody mx_AppLoading'>
|
||||||
</div>
|
<MessageSpinner msg='Loading...' />
|
||||||
);
|
</div>
|
||||||
} else if (this.state.hasPermissionToLoad == true) {
|
);
|
||||||
if (this.isMixedContent()) {
|
} else if (this.state.hasPermissionToLoad == true) {
|
||||||
|
if (this.isMixedContent()) {
|
||||||
|
appTileBody = (
|
||||||
|
<div className="mx_AppTileBody">
|
||||||
|
<AppWarning
|
||||||
|
errorMsg="Error - Mixed content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
appTileBody = (
|
||||||
|
<div className="mx_AppTileBody">
|
||||||
|
<iframe
|
||||||
|
ref="appFrame"
|
||||||
|
src={safeWidgetUrl}
|
||||||
|
allowFullScreen="true"
|
||||||
|
sandbox={sandboxFlags}
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className="mx_AppTileBody">
|
<div className="mx_AppTileBody">
|
||||||
<AppWarning
|
<AppPermission
|
||||||
errorMsg="Error - Mixed content"
|
isRoomEncrypted={isRoomEncrypted}
|
||||||
|
url={this.state.widgetUrl}
|
||||||
|
onPermissionGranted={this._grantWidgetPermission}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
appTileBody = (
|
|
||||||
<div className="mx_AppTileBody">
|
|
||||||
<iframe
|
|
||||||
ref="appFrame"
|
|
||||||
src={safeWidgetUrl}
|
|
||||||
allowFullScreen="true"
|
|
||||||
sandbox={sandboxFlags}
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
appTileBody = (
|
|
||||||
<div className="mx_AppTileBody">
|
|
||||||
<AppPermission
|
|
||||||
url={this.state.widgetUrl}
|
|
||||||
onPermissionGranted={this._grantWidgetPermission}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// editing is done in scalar
|
// editing is done in scalar
|
||||||
|
@ -253,21 +311,20 @@ 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 className="mx_AppTileMenuBar">
|
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||||
{this.formatAppTileName()}
|
{ this.formatAppTileName() }
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
<span className="mx_Beta" alt={betaHelpMsg} title={betaHelpMsg}>β</span>
|
{ /* Edit widget */ }
|
||||||
{/* Edit widget */}
|
{ showEditButton && <img
|
||||||
{showEditButton && <img
|
|
||||||
src="img/edit.svg"
|
src="img/edit.svg"
|
||||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
width="8" height="8"
|
width="8" height="8"
|
||||||
alt={_t('Edit')}
|
alt={_t('Edit')}
|
||||||
title={_t('Edit')}
|
title={_t('Edit')}
|
||||||
onClick={this._onEditClick}
|
onClick={this._onEditClick}
|
||||||
/>}
|
/> }
|
||||||
|
|
||||||
{/* Delete widget */}
|
{ /* Delete widget */ }
|
||||||
<img src={deleteIcon}
|
<img src={deleteIcon}
|
||||||
className={deleteClasses}
|
className={deleteClasses}
|
||||||
width="8" height="8"
|
width="8" height="8"
|
||||||
|
@ -277,7 +334,7 @@ export default React.createClass({
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{appTileBody}
|
{ appTileBody }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,10 +6,10 @@ const AppWarning = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className='mx_AppPermissionWarning'>
|
<div className='mx_AppPermissionWarning'>
|
||||||
<div className='mx_AppPermissionWarningImage'>
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_AppPermissionWarningText'>
|
<div className='mx_AppPermissionWarningText'>
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>{props.errorMsg}</span>
|
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
149
src/components/views/elements/EditableItemList.js
Normal file
149
src/components/views/elements/EditableItemList.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
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 {_t} from '../../../languageHandler.js';
|
||||||
|
|
||||||
|
const EditableItem = React.createClass({
|
||||||
|
displayName: 'EditableItem',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
initialValue: PropTypes.string,
|
||||||
|
index: PropTypes.number,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onRemove: PropTypes.func,
|
||||||
|
onAdd: PropTypes.func,
|
||||||
|
|
||||||
|
addOnChange: PropTypes.bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
onChange: function(value) {
|
||||||
|
this.setState({ value });
|
||||||
|
if (this.props.onChange) this.props.onChange(value, this.props.index);
|
||||||
|
if (this.props.addOnChange && this.props.onAdd) this.props.onAdd(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function() {
|
||||||
|
if (this.props.onRemove) this.props.onRemove(this.props.index);
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function() {
|
||||||
|
if (this.props.onAdd) this.props.onAdd(this.state.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const EditableText = sdk.getComponent('elements.EditableText');
|
||||||
|
return <div className="mx_EditableItem">
|
||||||
|
<EditableText
|
||||||
|
className="mx_EditableItem_editable"
|
||||||
|
placeholderClassName="mx_EditableItem_editablePlaceholder"
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
blurToCancel={false}
|
||||||
|
editable={true}
|
||||||
|
initialValue={this.props.initialValue}
|
||||||
|
onValueChanged={this.onChange} />
|
||||||
|
{ this.props.onAdd ?
|
||||||
|
<div className="mx_EditableItem_addButton">
|
||||||
|
<img className="mx_filterFlipColor"
|
||||||
|
src="img/plus.svg" width="14" height="14"
|
||||||
|
alt={_t("Add")} onClick={this.onAdd} />
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div className="mx_EditableItem_removeButton">
|
||||||
|
<img className="mx_filterFlipColor"
|
||||||
|
src="img/cancel-small.svg" width="14" height="14"
|
||||||
|
alt={_t("Delete")} onClick={this.onRemove} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'EditableItemList',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
onNewItemChanged: PropTypes.func,
|
||||||
|
onItemAdded: PropTypes.func,
|
||||||
|
onItemEdited: PropTypes.func,
|
||||||
|
onItemRemoved: PropTypes. func,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
onItemAdded: () => {},
|
||||||
|
onItemEdited: () => {},
|
||||||
|
onItemRemoved: () => {},
|
||||||
|
onNewItemChanged: () => {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onItemAdded: function(value) {
|
||||||
|
this.props.onItemAdded(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
onItemEdited: function(value, index) {
|
||||||
|
if (value.length === 0) {
|
||||||
|
this.onItemRemoved(index);
|
||||||
|
} else {
|
||||||
|
this.props.onItemEdited(value, index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onItemRemoved: function(index) {
|
||||||
|
this.props.onItemRemoved(index);
|
||||||
|
},
|
||||||
|
|
||||||
|
onNewItemChanged: function(value) {
|
||||||
|
this.props.onNewItemChanged(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const editableItems = this.props.items.map((item, index) => {
|
||||||
|
return <EditableItem
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
initialValue={item}
|
||||||
|
onChange={this.onItemEdited}
|
||||||
|
onRemove={this.onItemRemoved}
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
/>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const label = this.props.items.length > 0 ?
|
||||||
|
this.props.itemsLabel : this.props.noItemsLabel;
|
||||||
|
|
||||||
|
return (<div className="mx_EditableItemList">
|
||||||
|
<div className="mx_EditableItemList_label">
|
||||||
|
{ label }
|
||||||
|
</div>
|
||||||
|
{ editableItems }
|
||||||
|
<EditableItem
|
||||||
|
key={-1}
|
||||||
|
initialValue={this.props.newItem}
|
||||||
|
onAdd={this.onItemAdded}
|
||||||
|
onChange={this.onNewItemChanged}
|
||||||
|
addOnChange={true}
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
/>
|
||||||
|
</div>);
|
||||||
|
},
|
||||||
|
});
|
|
@ -65,7 +65,9 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
if (nextProps.initialValue !== this.props.initialValue) {
|
if (nextProps.initialValue !== this.props.initialValue ||
|
||||||
|
nextProps.initialValue !== this.value
|
||||||
|
) {
|
||||||
this.value = nextProps.initialValue;
|
this.value = nextProps.initialValue;
|
||||||
if (this.refs.editable_div) {
|
if (this.refs.editable_div) {
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
|
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,12 +16,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {emojifyText} from '../../../HtmlUtils';
|
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||||
|
|
||||||
export default function EmojiText(props) {
|
export default function EmojiText(props) {
|
||||||
const {element, children, ...restProps} = props;
|
const {element, children, ...restProps} = props;
|
||||||
restProps.dangerouslySetInnerHTML = emojifyText(children);
|
|
||||||
return React.createElement(element, restProps);
|
// fast path: simple regex to detect strings that don't contain
|
||||||
|
// emoji and just return them
|
||||||
|
if (containsEmoji(children)) {
|
||||||
|
restProps.dangerouslySetInnerHTML = emojifyText(children);
|
||||||
|
return React.createElement(element, restProps);
|
||||||
|
} else {
|
||||||
|
return React.createElement(element, restProps, children);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EmojiText.propTypes = {
|
EmojiText.propTypes = {
|
||||||
|
|
290
src/components/views/elements/Flair.js
Normal file
290
src/components/views/elements/Flair.js
Normal file
|
@ -0,0 +1,290 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {MatrixClient} from 'matrix-js-sdk';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
|
const BULK_REQUEST_DEBOUNCE_MS = 200;
|
||||||
|
|
||||||
|
// Does the server support groups? Assume yes until we receive M_UNRECOGNIZED.
|
||||||
|
// If true, flair can function and we should keep sending requests for groups and avatars.
|
||||||
|
let groupSupport = true;
|
||||||
|
|
||||||
|
const USER_GROUPS_CACHE_BUST_MS = 1800000; // 30 mins
|
||||||
|
const GROUP_PROFILES_CACHE_BUST_MS = 1800000; // 30 mins
|
||||||
|
|
||||||
|
// TODO: Cache-busting based on time. (The server won't inform us of membership changes.)
|
||||||
|
// This applies to userGroups and groupProfiles. We can provide a slightly better UX by
|
||||||
|
// cache-busting when the current user joins/leaves a group.
|
||||||
|
const userGroups = {
|
||||||
|
// $userId: ['+group1:domain', '+group2:domain', ...]
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupProfiles = {
|
||||||
|
// $groupId: {
|
||||||
|
// avatar_url: 'mxc://...'
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Represents all unsettled promises to retrieve the groups for each userId. When a promise
|
||||||
|
// is settled, it is deleted from this object.
|
||||||
|
const usersPending = {
|
||||||
|
// $userId: {
|
||||||
|
// prom: Promise
|
||||||
|
// resolve: () => {}
|
||||||
|
// reject: () => {}
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
let debounceTimeoutID;
|
||||||
|
function getPublicisedGroupsCached(matrixClient, userId) {
|
||||||
|
if (userGroups[userId]) {
|
||||||
|
return Promise.resolve(userGroups[userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk lookup ongoing, return promise to resolve/reject
|
||||||
|
if (usersPending[userId]) {
|
||||||
|
return usersPending[userId].prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
usersPending[userId] = {};
|
||||||
|
usersPending[userId].prom = new Promise((resolve, reject) => {
|
||||||
|
usersPending[userId].resolve = resolve;
|
||||||
|
usersPending[userId].reject = reject;
|
||||||
|
}).then((groups) => {
|
||||||
|
userGroups[userId] = groups;
|
||||||
|
setTimeout(() => {
|
||||||
|
delete userGroups[userId];
|
||||||
|
}, USER_GROUPS_CACHE_BUST_MS);
|
||||||
|
return userGroups[userId];
|
||||||
|
}).catch((err) => {
|
||||||
|
throw err;
|
||||||
|
}).finally(() => {
|
||||||
|
delete usersPending[userId];
|
||||||
|
});
|
||||||
|
|
||||||
|
// This debounce will allow consecutive requests for the public groups of users that
|
||||||
|
// are sent in intervals of < BULK_REQUEST_DEBOUNCE_MS to be batched and only requested
|
||||||
|
// when no more requests are received within the next BULK_REQUEST_DEBOUNCE_MS. The naive
|
||||||
|
// implementation would do a request that only requested the groups for `userId`, leading
|
||||||
|
// to a worst and best case of 1 user per request. This implementation's worst is still
|
||||||
|
// 1 user per request but only if the requests are > BULK_REQUEST_DEBOUNCE_MS apart and the
|
||||||
|
// best case is N users per request.
|
||||||
|
//
|
||||||
|
// This is to reduce the number of requests made whilst trading off latency when viewing
|
||||||
|
// a Flair component.
|
||||||
|
if (debounceTimeoutID) clearTimeout(debounceTimeoutID);
|
||||||
|
debounceTimeoutID = setTimeout(() => {
|
||||||
|
batchedGetPublicGroups(matrixClient);
|
||||||
|
}, BULK_REQUEST_DEBOUNCE_MS);
|
||||||
|
|
||||||
|
return usersPending[userId].prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchedGetPublicGroups(matrixClient) {
|
||||||
|
// Take the userIds from the keys of usersPending
|
||||||
|
const usersInFlight = Object.keys(usersPending);
|
||||||
|
let resp = {
|
||||||
|
users: [],
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
resp = await matrixClient.getPublicisedGroups(usersInFlight);
|
||||||
|
} catch (err) {
|
||||||
|
// Propagate the same error to all usersInFlight
|
||||||
|
usersInFlight.forEach((userId) => {
|
||||||
|
usersPending[userId].reject(err);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updatedUserGroups = resp.users;
|
||||||
|
usersInFlight.forEach((userId) => {
|
||||||
|
usersPending[userId].resolve(updatedUserGroups[userId] || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGroupProfileCached(matrixClient, groupId) {
|
||||||
|
if (groupProfiles[groupId]) {
|
||||||
|
return groupProfiles[groupId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await matrixClient.getGroupProfile(groupId);
|
||||||
|
groupProfiles[groupId] = {
|
||||||
|
groupId,
|
||||||
|
avatarUrl: profile.avatar_url,
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
delete groupProfiles[groupId];
|
||||||
|
}, GROUP_PROFILES_CACHE_BUST_MS);
|
||||||
|
|
||||||
|
return groupProfiles[groupId];
|
||||||
|
}
|
||||||
|
|
||||||
|
class FlairAvatar extends React.Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.onClick = this.onClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
// Don't trigger onClick of parent element
|
||||||
|
ev.stopPropagation();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_group',
|
||||||
|
group_id: this.props.groupProfile.groupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const httpUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||||
|
this.props.groupProfile.avatarUrl, 14, 14, 'scale', false);
|
||||||
|
return <img
|
||||||
|
src={httpUrl}
|
||||||
|
width="14px"
|
||||||
|
height="14px"
|
||||||
|
onClick={this.onClick}
|
||||||
|
title={this.props.groupProfile.groupId} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FlairAvatar.propTypes = {
|
||||||
|
groupProfile: PropTypes.shape({
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
|
avatarUrl: PropTypes.string.isRequired,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
FlairAvatar.contextTypes = {
|
||||||
|
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Flair extends React.Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
profiles: [],
|
||||||
|
};
|
||||||
|
this.onRoomStateEvents = this.onRoomStateEvents.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unmounted = true;
|
||||||
|
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this._unmounted = false;
|
||||||
|
if (UserSettingsStore.isFeatureEnabled('feature_groups') && groupSupport) {
|
||||||
|
this._generateAvatars();
|
||||||
|
}
|
||||||
|
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRoomStateEvents(event) {
|
||||||
|
if (event.getType() === 'm.room.related_groups' && groupSupport) {
|
||||||
|
this._generateAvatars();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getGroupProfiles(groups) {
|
||||||
|
const profiles = [];
|
||||||
|
for (const groupId of groups) {
|
||||||
|
let groupProfile = null;
|
||||||
|
try {
|
||||||
|
groupProfile = await getGroupProfileCached(this.context.matrixClient, groupId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Could not get profile for group', groupId, err);
|
||||||
|
}
|
||||||
|
profiles.push(groupProfile);
|
||||||
|
}
|
||||||
|
return profiles.filter((p) => p !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _generateAvatars() {
|
||||||
|
let groups;
|
||||||
|
try {
|
||||||
|
groups = await getPublicisedGroupsCached(this.context.matrixClient, this.props.userId);
|
||||||
|
} catch (err) {
|
||||||
|
// Indicate whether the homeserver supports groups
|
||||||
|
if (err.errcode === 'M_UNRECOGNIZED') {
|
||||||
|
console.warn('Cannot display flair, server does not support groups');
|
||||||
|
groupSupport = false;
|
||||||
|
// Return silently to avoid spamming for non-supporting servers
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error('Could not get groups for user', this.props.userId, err);
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const profiles = await this._getGroupProfiles(groups);
|
||||||
|
if (!this.unmounted) {
|
||||||
|
this.setState({profiles});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.profiles.length === 0) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
const avatars = this.state.profiles.map((profile, index) => {
|
||||||
|
return <FlairAvatar key={index} groupProfile={profile} />;
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<span className="mx_Flair" style={{"marginLeft": "5px", "verticalAlign": "-3px"}}>
|
||||||
|
{ avatars }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flair.propTypes = {
|
||||||
|
userId: PropTypes.string,
|
||||||
|
|
||||||
|
// Whether to show only the flair associated with related groups of the given room,
|
||||||
|
// or all flair associated with a user.
|
||||||
|
showRelated: PropTypes.bool,
|
||||||
|
// The room that this flair will be displayed in. Optional. Only applies when showRelated = true.
|
||||||
|
roomId: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using
|
||||||
|
// this.context.matrixClient everywhere instead of this.props.matrixClient.
|
||||||
|
// See https://github.com/vector-im/riot-web/issues/4951.
|
||||||
|
Flair.contextTypes = {
|
||||||
|
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||||
|
};
|
38
src/components/views/elements/GroupsButton.js
Normal file
38
src/components/views/elements/GroupsButton.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
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 sdk from '../../../index';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
const GroupsButton = function(props) {
|
||||||
|
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||||
|
return (
|
||||||
|
<ActionButton action="view_my_groups"
|
||||||
|
label={_t("Groups")}
|
||||||
|
iconPath="img/icons-groups.svg"
|
||||||
|
size={props.size}
|
||||||
|
tooltip={props.tooltip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GroupsButton.propTypes = {
|
||||||
|
size: PropTypes.string,
|
||||||
|
tooltip: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupsButton;
|
107
src/components/views/elements/ManageIntegsButton.js
Normal file
107
src/components/views/elements/ManageIntegsButton.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
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 classNames from 'classnames';
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
|
import ScalarMessaging from '../../../ScalarMessaging';
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import AccessibleButton from './AccessibleButton';
|
||||||
|
import TintableSvg from './TintableSvg';
|
||||||
|
|
||||||
|
export default class ManageIntegsButton extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
scalarError: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.onManageIntegrations = this.onManageIntegrations.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
ScalarMessaging.startListening();
|
||||||
|
this.scalarClient = null;
|
||||||
|
|
||||||
|
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||||
|
this.scalarClient = new ScalarAuthClient();
|
||||||
|
this.scalarClient.connect().done(() => {
|
||||||
|
this.forceUpdate();
|
||||||
|
}, (err) => {
|
||||||
|
this.setState({ scalarError: err});
|
||||||
|
console.error('Error whilst initialising scalarClient for ManageIntegsButton', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
ScalarMessaging.stopListening();
|
||||||
|
}
|
||||||
|
|
||||||
|
onManageIntegrations(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
|
Modal.createDialog(IntegrationsManager, {
|
||||||
|
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||||
|
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.roomId) :
|
||||||
|
null,
|
||||||
|
}, "mx_IntegrationsManager");
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let integrationsButton = <div />;
|
||||||
|
let integrationsWarningTriangle = <div />;
|
||||||
|
let integrationsErrorPopup = <div />;
|
||||||
|
if (this.scalarClient !== null) {
|
||||||
|
const integrationsButtonClasses = classNames({
|
||||||
|
mx_RoomHeader_button: true,
|
||||||
|
mx_RoomSettings_integrationsButton_error: !!this.state.scalarError,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
||||||
|
integrationsWarningTriangle = <img src="img/warning.svg" title={_t('Integrations Error')} width="17" />;
|
||||||
|
// Popup shown when hovering over integrationsButton_error (via CSS)
|
||||||
|
integrationsErrorPopup = (
|
||||||
|
<span className="mx_RoomSettings_integrationsButton_errorPopup">
|
||||||
|
{ _t('Could not connect to the integration server') }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
integrationsButton = (
|
||||||
|
<AccessibleButton className={integrationsButtonClasses} onClick={this.onManageIntegrations} title={_t('Manage Integrations')}>
|
||||||
|
<TintableSvg src="img/icons-apps.svg" width="35" height="35" />
|
||||||
|
{ integrationsWarningTriangle }
|
||||||
|
{ integrationsErrorPopup }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationsButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManageIntegsButton.propTypes = {
|
||||||
|
roomId: PropTypes.string.isRequired,
|
||||||
|
};
|
|
@ -34,11 +34,13 @@ module.exports = React.createClass({
|
||||||
threshold: React.PropTypes.number,
|
threshold: React.PropTypes.number,
|
||||||
// Called when the MELS expansion is toggled
|
// Called when the MELS expansion is toggled
|
||||||
onToggle: React.PropTypes.func,
|
onToggle: React.PropTypes.func,
|
||||||
|
// Whether or not to begin with state.expanded=true
|
||||||
|
startExpanded: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
expanded: false,
|
expanded: Boolean(this.props.startExpanded),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -376,7 +378,7 @@ module.exports = React.createClass({
|
||||||
return items[0];
|
return items[0];
|
||||||
} else if (remaining) {
|
} else if (remaining) {
|
||||||
items = items.slice(0, itemLimit);
|
items = items.slice(0, itemLimit);
|
||||||
return (remaining > 1)
|
return (remaining > 1)
|
||||||
? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } )
|
? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } )
|
||||||
: _t("%(items)s and one other", { items: items.join(', ') });
|
: _t("%(items)s and one other", { items: items.join(', ') });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -26,8 +26,8 @@ module.exports = React.createClass({
|
||||||
const msg = this.props.msg || "Loading...";
|
const msg = this.props.msg || "Loading...";
|
||||||
return (
|
return (
|
||||||
<div className="mx_Spinner">
|
<div className="mx_Spinner">
|
||||||
<div className="mx_Spinner_Msg">{msg}</div>
|
<div className="mx_Spinner_Msg">{ msg }</div>
|
||||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
|
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -167,11 +167,11 @@ const Pill = React.createClass({
|
||||||
userId = member.userId;
|
userId = member.userId;
|
||||||
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
|
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
|
||||||
if (this.props.shouldShowPillAvatar) {
|
if (this.props.shouldShowPillAvatar) {
|
||||||
avatar = <MemberAvatar member={member} width={16} height={16}/>;
|
avatar = <MemberAvatar member={member} width={16} height={16} />;
|
||||||
}
|
}
|
||||||
pillClass = 'mx_UserPill';
|
pillClass = 'mx_UserPill';
|
||||||
href = null;
|
href = null;
|
||||||
onClick = this.onUserPillClicked.bind(this);
|
onClick = this.onUserPillClicked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -180,7 +180,7 @@ const Pill = React.createClass({
|
||||||
if (room) {
|
if (room) {
|
||||||
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||||
if (this.props.shouldShowPillAvatar) {
|
if (this.props.shouldShowPillAvatar) {
|
||||||
avatar = <RoomAvatar room={room} width={16} height={16}/>;
|
avatar = <RoomAvatar room={room} width={16} height={16} />;
|
||||||
}
|
}
|
||||||
pillClass = 'mx_RoomPill';
|
pillClass = 'mx_RoomPill';
|
||||||
}
|
}
|
||||||
|
@ -195,12 +195,12 @@ const Pill = React.createClass({
|
||||||
if (this.state.pillType) {
|
if (this.state.pillType) {
|
||||||
return this.props.inMessage ?
|
return this.props.inMessage ?
|
||||||
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
|
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
|
||||||
{avatar}
|
{ avatar }
|
||||||
{linkText}
|
{ linkText }
|
||||||
</a> :
|
</a> :
|
||||||
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
||||||
{avatar}
|
{ avatar }
|
||||||
{linkText}
|
{ linkText }
|
||||||
</span>;
|
</span>;
|
||||||
} else {
|
} else {
|
||||||
// Deliberately render nothing if the URL isn't recognised
|
// Deliberately render nothing if the URL isn't recognised
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -13,7 +14,9 @@ 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.
|
||||||
*/
|
*/
|
||||||
var React = require('react');
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -21,12 +24,21 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// The number of elements to show before truncating. If negative, no truncation is done.
|
// The number of elements to show before truncating. If negative, no truncation is done.
|
||||||
truncateAt: React.PropTypes.number,
|
truncateAt: PropTypes.number,
|
||||||
// The className to apply to the wrapping div
|
// The className to apply to the wrapping div
|
||||||
className: React.PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
// A function that returns the children to be rendered into the element.
|
||||||
|
// function getChildren(start: number, end: number): Array<React.Node>
|
||||||
|
// The start element is included, the end is not (as in `slice`).
|
||||||
|
// If omitted, the React child elements will be used. This parameter can be used
|
||||||
|
// to avoid creating unnecessary React elements.
|
||||||
|
getChildren: PropTypes.func,
|
||||||
|
// A function that should return the total number of child element available.
|
||||||
|
// Required if getChildren is supplied.
|
||||||
|
getChildCount: PropTypes.func,
|
||||||
// A function which will be invoked when an overflow element is required.
|
// A function which will be invoked when an overflow element is required.
|
||||||
// This will be inserted after the children.
|
// This will be inserted after the children.
|
||||||
createOverflowElement: React.PropTypes.func
|
createOverflowElement: PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -34,40 +46,56 @@ module.exports = React.createClass({
|
||||||
truncateAt: 2,
|
truncateAt: 2,
|
||||||
createOverflowElement: function(overflowCount, totalCount) {
|
createOverflowElement: function(overflowCount, totalCount) {
|
||||||
return (
|
return (
|
||||||
<div>{_t("And %(count)s more...", {count: overflowCount})}</div>
|
<div>{ _t("And %(count)s more...", {count: overflowCount}) }</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_getChildren: function(start, end) {
|
||||||
|
if (this.props.getChildren && this.props.getChildCount) {
|
||||||
|
return this.props.getChildren(start, end);
|
||||||
|
} else {
|
||||||
|
// XXX: I'm not sure why anything would pass null into this, it seems
|
||||||
|
// like a bizzare case to handle, but I'm preserving the behaviour.
|
||||||
|
// (see commit 38d5c7d5c5d5a34dc16ef5d46278315f5c57f542)
|
||||||
|
return React.Children.toArray(this.props.children).filter((c) => {
|
||||||
|
return c != null;
|
||||||
|
}).slice(start, end);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getChildCount: function() {
|
||||||
|
if (this.props.getChildren && this.props.getChildCount) {
|
||||||
|
return this.props.getChildCount();
|
||||||
|
} else {
|
||||||
|
return React.Children.toArray(this.props.children).filter((c) => {
|
||||||
|
return c != null;
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var childsJsx = this.props.children;
|
let overflowNode = null;
|
||||||
var overflowJsx;
|
|
||||||
var childArray = React.Children.toArray(this.props.children).filter((c) => {
|
|
||||||
return c != null;
|
|
||||||
});
|
|
||||||
|
|
||||||
var childCount = childArray.length;
|
|
||||||
|
|
||||||
|
const totalChildren = this._getChildCount();
|
||||||
|
let upperBound = totalChildren;
|
||||||
if (this.props.truncateAt >= 0) {
|
if (this.props.truncateAt >= 0) {
|
||||||
var overflowCount = childCount - this.props.truncateAt;
|
const overflowCount = totalChildren - this.props.truncateAt;
|
||||||
|
|
||||||
if (overflowCount > 1) {
|
if (overflowCount > 1) {
|
||||||
overflowJsx = this.props.createOverflowElement(
|
overflowNode = this.props.createOverflowElement(
|
||||||
overflowCount, childCount
|
overflowCount, totalChildren,
|
||||||
);
|
);
|
||||||
|
upperBound = this.props.truncateAt;
|
||||||
// cut out the overflow elements
|
|
||||||
childArray.splice(childCount - overflowCount, overflowCount);
|
|
||||||
childsJsx = childArray; // use what is left
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const childNodes = this._getChildren(0, upperBound);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.props.className}>
|
<div className={this.props.className}>
|
||||||
{childsJsx}
|
{ childNodes }
|
||||||
{overflowJsx}
|
{ overflowNode }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
70
src/components/views/groups/GroupInviteTile.js
Normal file
70
src/components/views/groups/GroupInviteTile.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
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 dis from '../../../dispatcher';
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'GroupInviteTile',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
group: PropTypes.object.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick: function(e) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_group',
|
||||||
|
group_id: this.props.group.groupId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
|
|
||||||
|
const av = (
|
||||||
|
<BaseAvatar name={this.props.group.name} width={24} height={24}
|
||||||
|
url={this.props.group.avatarUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const label = <EmojiText
|
||||||
|
element="div"
|
||||||
|
title={this.props.group.name}
|
||||||
|
className="mx_GroupInviteTile_name"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
{ this.props.group.name }
|
||||||
|
</EmojiText>;
|
||||||
|
|
||||||
|
const badge = <div className="mx_GroupInviteTile_badge">!</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccessibleButton className="mx_GroupInviteTile" onClick={this.onClick}>
|
||||||
|
<div className="mx_GroupInviteTile_avatarContainer">
|
||||||
|
{ av }
|
||||||
|
</div>
|
||||||
|
<div className="mx_GroupInviteTile_nameContainer">
|
||||||
|
{ label }
|
||||||
|
{ badge }
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
195
src/components/views/groups/GroupMemberInfo.js
Normal file
195
src/components/views/groups/GroupMemberInfo.js
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
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 PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { GroupMemberType } from '../../../groups';
|
||||||
|
import { groupMemberFromApiObject } from '../../../groups';
|
||||||
|
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = withMatrixClient(React.createClass({
|
||||||
|
displayName: 'GroupMemberInfo',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
matrixClient: PropTypes.object.isRequired,
|
||||||
|
groupId: PropTypes.string,
|
||||||
|
groupMember: GroupMemberType,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
fetching: false,
|
||||||
|
removingUser: false,
|
||||||
|
groupMembers: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._fetchMembers();
|
||||||
|
},
|
||||||
|
|
||||||
|
_fetchMembers: function() {
|
||||||
|
this.setState({fetching: true});
|
||||||
|
this.props.matrixClient.getGroupUsers(this.props.groupId).then((result) => {
|
||||||
|
this.setState({
|
||||||
|
groupMembers: result.chunk.map((apiMember) => {
|
||||||
|
return groupMemberFromApiObject(apiMember);
|
||||||
|
}),
|
||||||
|
fetching: false,
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({fetching: false});
|
||||||
|
console.error("Failed to get group groupMember list: ", e);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onKick: function() {
|
||||||
|
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||||
|
Modal.createDialog(ConfirmUserActionDialog, {
|
||||||
|
groupMember: this.props.groupMember,
|
||||||
|
action: _t('Remove from group'),
|
||||||
|
danger: true,
|
||||||
|
onFinished: (proceed) => {
|
||||||
|
if (!proceed) return;
|
||||||
|
|
||||||
|
this.setState({removingUser: true});
|
||||||
|
this.props.matrixClient.removeUserFromGroup(
|
||||||
|
this.props.groupId, this.props.groupMember.userId,
|
||||||
|
).then(() => {
|
||||||
|
// return to the user list
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_user",
|
||||||
|
member: null,
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
|
||||||
|
title: _t('Error'),
|
||||||
|
description: _t('Failed to remove user from group'),
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
this.setState({removingUser: false});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCancel: function(e) {
|
||||||
|
// Go back to the user list
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_user",
|
||||||
|
member: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomTileClick(roomId) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
if (this.state.fetching || this.state.removingUser) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
if (!this.state.groupMembers) return null;
|
||||||
|
|
||||||
|
const targetIsInGroup = this.state.groupMembers.some((m) => {
|
||||||
|
return m.userId === this.props.groupMember.userId;
|
||||||
|
});
|
||||||
|
|
||||||
|
let kickButton;
|
||||||
|
let adminButton;
|
||||||
|
|
||||||
|
if (targetIsInGroup) {
|
||||||
|
kickButton = (
|
||||||
|
<AccessibleButton className="mx_MemberInfo_field"
|
||||||
|
onClick={this._onKick}>
|
||||||
|
{ _t('Remove from group') }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
// No make/revoke admin API yet
|
||||||
|
/*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
|
||||||
|
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||||
|
{giveOpLabel}
|
||||||
|
</AccessibleButton>;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
let adminTools;
|
||||||
|
if (kickButton || adminButton) {
|
||||||
|
adminTools =
|
||||||
|
<div className="mx_MemberInfo_adminTools">
|
||||||
|
<h3>{ _t("Admin Tools") }</h3>
|
||||||
|
|
||||||
|
<div className="mx_MemberInfo_buttons">
|
||||||
|
{ kickButton }
|
||||||
|
{ adminButton }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarUrl = this.props.matrixClient.mxcUrlToHttp(
|
||||||
|
this.props.groupMember.avatarUrl,
|
||||||
|
36, 36, 'crop',
|
||||||
|
);
|
||||||
|
|
||||||
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
const avatar = (
|
||||||
|
<BaseAvatar name={this.props.groupMember.userId} width={36} height={36}
|
||||||
|
url={avatarUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupMemberName = (
|
||||||
|
this.props.groupMember.displayname || this.props.groupMember.userId
|
||||||
|
);
|
||||||
|
|
||||||
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
|
return (
|
||||||
|
<div className="mx_MemberInfo">
|
||||||
|
<GeminiScrollbar autoshow={true}>
|
||||||
|
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
|
||||||
|
<img src="img/cancel.svg" width="18" height="18" />
|
||||||
|
</AccessibleButton>
|
||||||
|
<div className="mx_MemberInfo_avatar">
|
||||||
|
{ avatar }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EmojiText element="h2">{ groupMemberName }</EmojiText>
|
||||||
|
|
||||||
|
<div className="mx_MemberInfo_profile">
|
||||||
|
<div className="mx_MemberInfo_profileField">
|
||||||
|
{ this.props.groupMember.userId }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ adminTools }
|
||||||
|
</GeminiScrollbar>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
155
src/components/views/groups/GroupMemberList.js
Normal file
155
src/components/views/groups/GroupMemberList.js
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd.
|
||||||
|
Copyright 2017 New Vector Ltd.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import { groupMemberFromApiObject } from '../../../groups';
|
||||||
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||||
|
|
||||||
|
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||||
|
|
||||||
|
export default withMatrixClient(React.createClass({
|
||||||
|
displayName: 'GroupMemberList',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
matrixClient: PropTypes.object.isRequired,
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
fetching: false,
|
||||||
|
members: null,
|
||||||
|
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
|
this._fetchMembers();
|
||||||
|
},
|
||||||
|
|
||||||
|
_fetchMembers: function() {
|
||||||
|
this.setState({fetching: true});
|
||||||
|
this.props.matrixClient.getGroupUsers(this.props.groupId).then((result) => {
|
||||||
|
this.setState({
|
||||||
|
members: result.chunk.map((apiMember) => {
|
||||||
|
return groupMemberFromApiObject(apiMember);
|
||||||
|
}),
|
||||||
|
fetching: false,
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({fetching: false});
|
||||||
|
console.error("Failed to get group member list: " + e);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_createOverflowTile: function(overflowCount, totalCount) {
|
||||||
|
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||||
|
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||||
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
|
return (
|
||||||
|
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||||
|
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
||||||
|
} name={text} presenceState="online" suppressOnHover={true}
|
||||||
|
onClick={this._showFullMemberList} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_showFullMemberList: function() {
|
||||||
|
this.setState({
|
||||||
|
truncateAt: -1,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onSearchQueryChanged: function(ev) {
|
||||||
|
this.setState({ searchQuery: ev.target.value });
|
||||||
|
},
|
||||||
|
|
||||||
|
makeGroupMemberTiles: function(query) {
|
||||||
|
const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile");
|
||||||
|
query = (query || "").toLowerCase();
|
||||||
|
|
||||||
|
let memberList = this.state.members;
|
||||||
|
if (query) {
|
||||||
|
memberList = memberList.filter((m) => {
|
||||||
|
const matchesName = m.displayname.toLowerCase().indexOf(query) !== -1;
|
||||||
|
const matchesId = m.userId.toLowerCase().includes(query);
|
||||||
|
|
||||||
|
if (!matchesName && !matchesId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
memberList = memberList.map((m) => {
|
||||||
|
return (
|
||||||
|
<GroupMemberTile key={m.userId} groupId={this.props.groupId} member={m} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
memberList.sort((a, b) => {
|
||||||
|
// TODO: should put admins at the top: we don't yet have that info
|
||||||
|
if (a < b) {
|
||||||
|
return -1;
|
||||||
|
} else if (a > b) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return memberList;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
if (this.state.fetching) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
return (<div className="mx_MemberList">
|
||||||
|
<Spinner />
|
||||||
|
</div>);
|
||||||
|
} else if (this.state.members === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputBox = (
|
||||||
|
<form autoComplete="off">
|
||||||
|
<input className="mx_GroupMemberList_query" id="mx_GroupMemberList_query" type="text"
|
||||||
|
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||||
|
placeholder={_t('Filter group members')} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||||
|
return (
|
||||||
|
<div className="mx_MemberList">
|
||||||
|
{ inputBox }
|
||||||
|
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
|
||||||
|
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
|
||||||
|
createOverflowElement={this._createOverflowTile}>
|
||||||
|
{ this.makeGroupMemberTiles(this.state.searchQuery) }
|
||||||
|
</TruncatedList>
|
||||||
|
</GeminiScrollbar>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
70
src/components/views/groups/GroupMemberTile.js
Normal file
70
src/components/views/groups/GroupMemberTile.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
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 dis from '../../../dispatcher';
|
||||||
|
import { GroupMemberType } from '../../../groups';
|
||||||
|
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||||
|
|
||||||
|
export default withMatrixClient(React.createClass({
|
||||||
|
displayName: 'GroupMemberTile',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
matrixClient: PropTypes.object,
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
|
member: GroupMemberType.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick: function(e) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_group_user',
|
||||||
|
member: this.props.member,
|
||||||
|
groupId: this.props.groupId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
const EntityTile = sdk.getComponent('rooms.EntityTile');
|
||||||
|
|
||||||
|
const name = this.props.member.displayname || this.props.member.userId;
|
||||||
|
const avatarUrl = this.props.matrixClient.mxcUrlToHttp(
|
||||||
|
this.props.member.avatarUrl,
|
||||||
|
36, 36, 'crop',
|
||||||
|
);
|
||||||
|
|
||||||
|
const av = (
|
||||||
|
<BaseAvatar name={this.props.member.userId}
|
||||||
|
width={36} height={36}
|
||||||
|
url={avatarUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EntityTile presenceState="online"
|
||||||
|
avatarJsx={av} onClick={this.onClick}
|
||||||
|
name={name} powerLevel={0} suppressOnHover={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
143
src/components/views/groups/GroupRoomList.js
Normal file
143
src/components/views/groups/GroupRoomList.js
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
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 { _t } from '../../../languageHandler';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import { groupRoomFromApiObject } from '../../../groups';
|
||||||
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {MatrixClient} from 'matrix-js-sdk';
|
||||||
|
|
||||||
|
const INITIAL_LOAD_NUM_ROOMS = 30;
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
contextTypes: {
|
||||||
|
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
fetching: false,
|
||||||
|
rooms: null,
|
||||||
|
truncateAt: INITIAL_LOAD_NUM_ROOMS,
|
||||||
|
searchQuery: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
|
this._fetchRooms();
|
||||||
|
},
|
||||||
|
|
||||||
|
_fetchRooms: function() {
|
||||||
|
this.setState({fetching: true});
|
||||||
|
this.context.matrixClient.getGroupRooms(this.props.groupId).then((result) => {
|
||||||
|
this.setState({
|
||||||
|
rooms: result.chunk.map((apiRoom) => {
|
||||||
|
return groupRoomFromApiObject(apiRoom);
|
||||||
|
}),
|
||||||
|
fetching: false,
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({fetching: false});
|
||||||
|
console.error("Failed to get group room list: ", e);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_createOverflowTile: function(overflowCount, totalCount) {
|
||||||
|
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||||
|
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||||
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
|
return (
|
||||||
|
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||||
|
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
||||||
|
} name={text} presenceState="online" suppressOnHover={true}
|
||||||
|
onClick={this._showFullRoomList} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_showFullRoomList: function() {
|
||||||
|
this.setState({
|
||||||
|
truncateAt: -1,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onSearchQueryChanged: function(ev) {
|
||||||
|
this.setState({ searchQuery: ev.target.value });
|
||||||
|
},
|
||||||
|
|
||||||
|
makeGroupRoomTiles: function(query) {
|
||||||
|
const GroupRoomTile = sdk.getComponent("groups.GroupRoomTile");
|
||||||
|
query = (query || "").toLowerCase();
|
||||||
|
|
||||||
|
let roomList = this.state.rooms;
|
||||||
|
if (query) {
|
||||||
|
roomList = roomList.filter((room) => {
|
||||||
|
const matchesName = (room.name || "").toLowerCase().include(query);
|
||||||
|
const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
|
||||||
|
return matchesName || matchesAlias;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
roomList = roomList.map((groupRoom, index) => {
|
||||||
|
return (
|
||||||
|
<GroupRoomTile
|
||||||
|
key={index}
|
||||||
|
groupId={this.props.groupId}
|
||||||
|
groupRoom={groupRoom} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return roomList;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
if (this.state.fetching) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
return (<div className="mx_GroupRoomList">
|
||||||
|
<Spinner />
|
||||||
|
</div>);
|
||||||
|
} else if (this.state.rooms === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputBox = (
|
||||||
|
<form autoComplete="off">
|
||||||
|
<input className="mx_GroupRoomList_query" id="mx_GroupRoomList_query" type="text"
|
||||||
|
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||||
|
placeholder={_t('Filter group rooms')} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||||
|
return (
|
||||||
|
<div className="mx_GroupRoomList">
|
||||||
|
{ inputBox }
|
||||||
|
<GeminiScrollbar autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
|
||||||
|
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
|
||||||
|
createOverflowElement={this._createOverflowTile}>
|
||||||
|
{ this.makeGroupRoomTiles(this.state.searchQuery) }
|
||||||
|
</TruncatedList>
|
||||||
|
</GeminiScrollbar>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
135
src/components/views/groups/GroupRoomTile.js
Normal file
135
src/components/views/groups/GroupRoomTile.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
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 {MatrixClient} from 'matrix-js-sdk';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import { GroupRoomType } from '../../../groups';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
|
const GroupRoomTile = React.createClass({
|
||||||
|
displayName: 'GroupRoomTile',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
groupId: PropTypes.string.isRequired,
|
||||||
|
groupRoom: GroupRoomType.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
name: this.calculateRoomName(this.props.groupRoom),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
this.setState({
|
||||||
|
name: this.calculateRoomName(newProps.groupRoom),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
calculateRoomName: function(groupRoom) {
|
||||||
|
return groupRoom.name || groupRoom.canonicalAlias || _t("Unnamed Room");
|
||||||
|
},
|
||||||
|
|
||||||
|
removeRoomFromGroup: function() {
|
||||||
|
const groupId = this.props.groupId;
|
||||||
|
const roomName = this.state.name;
|
||||||
|
const roomId = this.props.groupRoom.roomId;
|
||||||
|
this.context.matrixClient
|
||||||
|
.removeRoomFromGroup(groupId, roomId)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`Error whilst removing ${roomId} from ${groupId}`, err);
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
|
||||||
|
title: _t("Failed to remove room from group"),
|
||||||
|
description: _t("Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick: function(e) {
|
||||||
|
let roomId;
|
||||||
|
let roomAlias;
|
||||||
|
if (this.props.groupRoom.canonicalAlias) {
|
||||||
|
roomAlias = this.props.groupRoom.canonicalAlias;
|
||||||
|
} else {
|
||||||
|
roomId = this.props.groupRoom.roomId;
|
||||||
|
}
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
room_alias: roomAlias,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onDeleteClick: function(e) {
|
||||||
|
const groupId = this.props.groupId;
|
||||||
|
const roomName = this.state.name;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, {
|
||||||
|
title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", {roomName, groupId}),
|
||||||
|
description: _t("Removing a room from the group will also remove it from the group page."),
|
||||||
|
button: _t("Remove"),
|
||||||
|
onFinished: (success) => {
|
||||||
|
if (success) {
|
||||||
|
this.removeRoomFromGroup();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||||
|
this.props.groupRoom.avatarUrl,
|
||||||
|
36, 36, 'crop',
|
||||||
|
);
|
||||||
|
|
||||||
|
const av = (
|
||||||
|
<BaseAvatar name={this.state.name}
|
||||||
|
width={36} height={36}
|
||||||
|
url={avatarUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccessibleButton className="mx_GroupRoomTile" onClick={this.onClick}>
|
||||||
|
<div className="mx_GroupRoomTile_avatar">
|
||||||
|
{ av }
|
||||||
|
</div>
|
||||||
|
<div className="mx_GroupRoomTile_name">
|
||||||
|
{ this.state.name }
|
||||||
|
</div>
|
||||||
|
<AccessibleButton className="mx_GroupRoomTile_delete" onClick={this.onDeleteClick}>
|
||||||
|
<img src="img/cancel-small.svg" />
|
||||||
|
</AccessibleButton>
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
GroupRoomTile.contextTypes = {
|
||||||
|
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default GroupRoomTile;
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
import { _t, _tJsx } from '../../../languageHandler';
|
import { _t, _tJsx } from '../../../languageHandler';
|
||||||
|
|
||||||
var DIV_ID = 'mx_recaptcha';
|
var DIV_ID = 'mx_recaptcha';
|
||||||
|
@ -66,11 +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
|
||||||
warning.innerHTML = _tJsx(
|
ReactDOM.render(_tJsx(
|
||||||
"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>/,
|
/<a>(.*?)<\/a>/,
|
||||||
(sub) => { return "<a href='https://riot.im/app'>{ sub }</a>"; }
|
(sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning);
|
||||||
);
|
|
||||||
this.refs.recaptchaContainer.appendChild(warning);
|
this.refs.recaptchaContainer.appendChild(warning);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -25,7 +25,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_Login_links">
|
<div className="mx_Login_links">
|
||||||
<a href="https://matrix.org">{_t("powered by Matrix")}</a>
|
<a href="https://matrix.org">{ _t("powered by Matrix") }</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import Flair from '../elements/Flair.js';
|
||||||
|
|
||||||
export default function SenderProfile(props) {
|
export default function SenderProfile(props) {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
|
@ -30,8 +31,17 @@ export default function SenderProfile(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmojiText className="mx_SenderProfile" dir="auto"
|
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
|
||||||
onClick={props.onClick}>{`${name || ''} ${props.aux || ''}`}</EmojiText>
|
<EmojiText className="mx_SenderProfile_name">{ name || '' }</EmojiText>
|
||||||
|
{ props.enableFlair ?
|
||||||
|
<Flair
|
||||||
|
userId={mxEvent.getSender()}
|
||||||
|
roomId={mxEvent.getRoomId()}
|
||||||
|
showRelated={true} />
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{ props.aux ? <EmojiText className="mx_SenderProfile_aux"> { props.aux }</EmojiText> : null }
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import dis from '../../../dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import UserSettingsStore from "../../../UserSettingsStore";
|
import UserSettingsStore from "../../../UserSettingsStore";
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
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';
|
||||||
|
|
||||||
|
@ -72,12 +73,16 @@ module.exports = React.createClass({
|
||||||
textArea.value = text;
|
textArea.value = text;
|
||||||
document.body.appendChild(textArea);
|
document.body.appendChild(textArea);
|
||||||
textArea.select();
|
textArea.select();
|
||||||
|
|
||||||
|
let successful = false;
|
||||||
try {
|
try {
|
||||||
const successful = document.execCommand('copy');
|
successful = document.execCommand('copy');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Unable to copy');
|
console.log('Unable to copy');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
document.body.removeChild(textArea);
|
||||||
|
return successful;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -113,14 +118,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
// add event handlers to the 'copy code' buttons
|
this._addCodeCopyButton();
|
||||||
const buttons = ReactDOM.findDOMNode(this).getElementsByClassName("mx_EventTile_copyButton");
|
|
||||||
for (let i = 0; i < buttons.length; i++) {
|
|
||||||
buttons[i].onclick = (e) => {
|
|
||||||
const copyCode = buttons[i].parentNode.getElementsByTagName("code")[0];
|
|
||||||
this.copyToClipboard(copyCode.textContent);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -257,6 +255,33 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_addCodeCopyButton() {
|
||||||
|
// Add 'copy' buttons to pre blocks
|
||||||
|
ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre').forEach((p) => {
|
||||||
|
const button = document.createElement("span");
|
||||||
|
button.className = "mx_EventTile_copyButton";
|
||||||
|
button.onclick = (e) => {
|
||||||
|
const copyCode = button.parentNode.getElementsByTagName("code")[0];
|
||||||
|
const successful = this.copyToClipboard(copyCode.textContent);
|
||||||
|
|
||||||
|
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||||
|
const buttonRect = e.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||||
|
const x = buttonRect.right + window.pageXOffset;
|
||||||
|
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||||
|
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
|
||||||
|
chevronOffset: 10,
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||||
|
});
|
||||||
|
e.target.onmouseout = close;
|
||||||
|
};
|
||||||
|
p.appendChild(button);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onCancelClick: function(event) {
|
onCancelClick: function(event) {
|
||||||
this.setState({ widgetHidden: true });
|
this.setState({ widgetHidden: true });
|
||||||
// FIXME: persist this somewhere smarter than local storage
|
// FIXME: persist this somewhere smarter than local storage
|
||||||
|
|
|
@ -23,10 +23,15 @@ module.exports = React.createClass({
|
||||||
displayName: 'UnknownBody',
|
displayName: 'UnknownBody',
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
let tooltip = _t("Removed or unknown message type");
|
||||||
|
if (this.props.mxEvent.isRedacted()) {
|
||||||
|
tooltip = _t("Message removed by %(userId)s", {userId: this.props.mxEvent.getSender()});
|
||||||
|
}
|
||||||
|
|
||||||
const text = this.props.mxEvent.getContent().body;
|
const text = this.props.mxEvent.getContent().body;
|
||||||
return (
|
return (
|
||||||
<span className="mx_UnknownBody" title={_t("Removed or unknown message type")}>
|
<span className="mx_UnknownBody" title={tooltip}>
|
||||||
{text}
|
{ text }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -136,24 +136,25 @@ module.exports = React.createClass({
|
||||||
return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases);
|
return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases);
|
||||||
},
|
},
|
||||||
|
|
||||||
onAliasAdded: function(alias) {
|
onNewAliasChanged: function(value) {
|
||||||
|
this.setState({newAlias: value});
|
||||||
|
},
|
||||||
|
|
||||||
|
onLocalAliasAdded: function(alias) {
|
||||||
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
||||||
|
|
||||||
if (this.isAliasValid(alias)) {
|
const localDomain = MatrixClientPeg.get().getDomain();
|
||||||
// add this alias to the domain to aliases dict
|
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||||
var domain = alias.replace(/^.*?:/, '');
|
this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || [];
|
||||||
// XXX: do we need to deep copy aliases before editing it?
|
this.state.domainToAliases[localDomain].push(alias);
|
||||||
this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || [];
|
|
||||||
this.state.domainToAliases[domain].push(alias);
|
|
||||||
this.setState({
|
|
||||||
domainToAliases: this.state.domainToAliases
|
|
||||||
});
|
|
||||||
|
|
||||||
// reset the add field
|
this.setState({
|
||||||
this.refs.add_alias.setValue(''); // FIXME
|
domainToAliases: this.state.domainToAliases,
|
||||||
}
|
// Reset the add field
|
||||||
else {
|
newAlias: "",
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
});
|
||||||
|
} else {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
|
Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
|
||||||
title: _t('Invalid alias format'),
|
title: _t('Invalid alias format'),
|
||||||
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
||||||
|
@ -161,15 +162,13 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onAliasChanged: function(domain, index, alias) {
|
onLocalAliasChanged: function(alias, index) {
|
||||||
if (alias === "") return; // hit the delete button to delete please
|
if (alias === "") return; // hit the delete button to delete please
|
||||||
var oldAlias;
|
const localDomain = MatrixClientPeg.get().getDomain();
|
||||||
if (this.isAliasValid(alias)) {
|
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||||
oldAlias = this.state.domainToAliases[domain][index];
|
this.state.domainToAliases[localDomain][index] = alias;
|
||||||
this.state.domainToAliases[domain][index] = alias;
|
} else {
|
||||||
}
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
else {
|
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
|
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
|
||||||
title: _t('Invalid address format'),
|
title: _t('Invalid address format'),
|
||||||
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
|
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
|
||||||
|
@ -177,15 +176,16 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onAliasDeleted: function(domain, index) {
|
onLocalAliasDeleted: function(index) {
|
||||||
|
const localDomain = MatrixClientPeg.get().getDomain();
|
||||||
// It's a bit naughty to directly manipulate this.state, and React would
|
// It's a bit naughty to directly manipulate this.state, and React would
|
||||||
// normally whine at you, but it can't see us doing the splice. Given we
|
// normally whine at you, but it can't see us doing the splice. Given we
|
||||||
// promptly setState anyway, it's just about acceptable. The alternative
|
// promptly setState anyway, it's just about acceptable. The alternative
|
||||||
// would be to arbitrarily deepcopy to a temp variable and then setState
|
// would be to arbitrarily deepcopy to a temp variable and then setState
|
||||||
// that, but why bother when we can cut this corner.
|
// that, but why bother when we can cut this corner.
|
||||||
var alias = this.state.domainToAliases[domain].splice(index, 1);
|
this.state.domainToAliases[localDomain].splice(index, 1);
|
||||||
this.setState({
|
this.setState({
|
||||||
domainToAliases: this.state.domainToAliases
|
domainToAliases: this.state.domainToAliases,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -198,6 +198,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var EditableText = sdk.getComponent("elements.EditableText");
|
var EditableText = sdk.getComponent("elements.EditableText");
|
||||||
|
var EditableItemList = sdk.getComponent("elements.EditableItemList");
|
||||||
var localDomain = MatrixClientPeg.get().getDomain();
|
var localDomain = MatrixClientPeg.get().getDomain();
|
||||||
|
|
||||||
var canonical_alias_section;
|
var canonical_alias_section;
|
||||||
|
@ -257,58 +258,24 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomSettings_aliasLabel">
|
<div className="mx_RoomSettings_aliasLabel">
|
||||||
{ _t('The main address for this room is') }: { canonical_alias_section }
|
{ _t('The main address for this room is') }: { canonical_alias_section }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomSettings_aliasLabel">
|
<EditableItemList
|
||||||
{ (this.state.domainToAliases[localDomain] &&
|
className={"mx_RoomSettings_localAliases"}
|
||||||
this.state.domainToAliases[localDomain].length > 0)
|
items={this.state.domainToAliases[localDomain] || []}
|
||||||
? _t('Local addresses for this room:')
|
newItem={this.state.newAlias}
|
||||||
: _t('This room has no local addresses') }
|
onNewItemChanged={this.onNewAliasChanged}
|
||||||
</div>
|
onItemAdded={this.onLocalAliasAdded}
|
||||||
<div className="mx_RoomSettings_aliasesTable">
|
onItemEdited={this.onLocalAliasChanged}
|
||||||
{ (this.state.domainToAliases[localDomain] || []).map((alias, i) => {
|
onItemRemoved={this.onLocalAliasDeleted}
|
||||||
var deleteButton;
|
itemsLabel={_t('Local addresses for this room:')}
|
||||||
if (this.props.canSetAliases) {
|
noItemsLabel={_t('This room has no local addresses')}
|
||||||
deleteButton = (
|
placeholder={_t(
|
||||||
<img src="img/cancel-small.svg" width="14" height="14"
|
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
|
||||||
alt={ _t('Delete') } onClick={ self.onAliasDeleted.bind(self, localDomain, i) } />
|
)}
|
||||||
);
|
/>
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="mx_RoomSettings_aliasesTableRow" key={ i }>
|
|
||||||
<EditableText
|
|
||||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
|
||||||
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
|
|
||||||
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
|
|
||||||
blurToCancel={ false }
|
|
||||||
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
|
|
||||||
editable={ self.props.canSetAliases }
|
|
||||||
initialValue={ alias } />
|
|
||||||
<div className="mx_RoomSettings_deleteAlias mx_filterFlipColor">
|
|
||||||
{ deleteButton }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{ this.props.canSetAliases ?
|
|
||||||
<div className="mx_RoomSettings_aliasesTableRow" key="new">
|
|
||||||
<EditableText
|
|
||||||
ref="add_alias"
|
|
||||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
|
||||||
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
|
|
||||||
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
|
|
||||||
blurToCancel={ false }
|
|
||||||
onValueChanged={ self.onAliasAdded } />
|
|
||||||
<div className="mx_RoomSettings_addAlias mx_filterFlipColor">
|
|
||||||
<img src="img/plus.svg" width="14" height="14" alt="Add"
|
|
||||||
onClick={ self.onAliasAdded.bind(self, undefined) }/>
|
|
||||||
</div>
|
|
||||||
</div> : ""
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ remote_aliases_section }
|
{ remote_aliases_section }
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
125
src/components/views/room_settings/RelatedGroupSettings.js
Normal file
125
src/components/views/room_settings/RelatedGroupSettings.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
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 {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
|
const GROUP_ID_REGEX = /\+\S+\:\S+/;
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'RelatedGroupSettings',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
roomId: React.PropTypes.string.isRequired,
|
||||||
|
canSetRelatedRooms: React.PropTypes.bool.isRequired,
|
||||||
|
relatedGroupsEvent: React.PropTypes.instanceOf(MatrixEvent),
|
||||||
|
},
|
||||||
|
|
||||||
|
contextTypes: {
|
||||||
|
matrixClient: React.PropTypes.instanceOf(MatrixClient),
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
canSetRelatedRooms: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
newGroupsList: this.props.relatedGroupsEvent ?
|
||||||
|
(this.props.relatedGroupsEvent.getContent().groups || []) : [],
|
||||||
|
newGroupId: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
saveSettings: function() {
|
||||||
|
return this.context.matrixClient.sendStateEvent(
|
||||||
|
this.props.roomId,
|
||||||
|
'm.room.related_groups',
|
||||||
|
{
|
||||||
|
groups: this.state.newGroupsList,
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
validateGroupId: function(groupId) {
|
||||||
|
if (!GROUP_ID_REGEX.test(groupId)) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Invalid related group ID', '', ErrorDialog, {
|
||||||
|
title: _t('Invalid group ID'),
|
||||||
|
description: _t('\'%(groupId)s\' is not a valid group ID', { groupId }),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
onNewGroupChanged: function(newGroupId) {
|
||||||
|
this.setState({ newGroupId });
|
||||||
|
},
|
||||||
|
|
||||||
|
onGroupAdded: function(groupId) {
|
||||||
|
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
newGroupsList: this.state.newGroupsList.concat([groupId]),
|
||||||
|
newGroupId: '',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onGroupEdited: function(groupId, index) {
|
||||||
|
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
newGroupsList: Object.assign(this.state.newGroupsList, {[index]: groupId}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onGroupDeleted: function(index) {
|
||||||
|
const newGroupsList = this.state.newGroupsList.slice();
|
||||||
|
newGroupsList.splice(index, 1),
|
||||||
|
this.setState({ newGroupsList });
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const localDomain = this.context.matrixClient.getDomain();
|
||||||
|
const EditableItemList = sdk.getComponent('elements.EditableItemList');
|
||||||
|
return (<div>
|
||||||
|
<h3>{ _t('Related Groups') }</h3>
|
||||||
|
<EditableItemList
|
||||||
|
items={this.state.newGroupsList}
|
||||||
|
className={"mx_RelatedGroupSettings"}
|
||||||
|
newItem={this.state.newGroupId}
|
||||||
|
onNewItemChanged={this.onNewGroupChanged}
|
||||||
|
onItemAdded={this.onGroupAdded}
|
||||||
|
onItemEdited={this.onGroupEdited}
|
||||||
|
onItemRemoved={this.onGroupDeleted}
|
||||||
|
itemsLabel={_t('Related groups for this room:')}
|
||||||
|
noItemsLabel={_t('This room has no related groups')}
|
||||||
|
placeholder={_t(
|
||||||
|
'New group ID (e.g. +foo:%(localDomain)s)', {localDomain},
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>);
|
||||||
|
},
|
||||||
|
});
|
|
@ -28,6 +28,8 @@ import ScalarMessaging from '../../../ScalarMessaging';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import WidgetUtils from '../../../WidgetUtils';
|
import WidgetUtils from '../../../WidgetUtils';
|
||||||
|
|
||||||
|
// The maximum number of widgets that can be added in a room
|
||||||
|
const MAX_WIDGETS = 2;
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'AppsDrawer',
|
displayName: 'AppsDrawer',
|
||||||
|
@ -51,19 +53,18 @@ module.exports = React.createClass({
|
||||||
this.scalarClient = null;
|
this.scalarClient = null;
|
||||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||||
this.scalarClient = new ScalarAuthClient();
|
this.scalarClient = new ScalarAuthClient();
|
||||||
this.scalarClient.connect().done(() => {
|
this.scalarClient.connect().then(() => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
if (this.state.apps && this.state.apps.length < 1) {
|
}).catch((e) => {
|
||||||
this.onClickAddWidget();
|
console.log("Failed to connect to integrations server");
|
||||||
}
|
// TODO -- Handle Scalar errors
|
||||||
// TODO -- Handle Scalar errors
|
// this.setState({
|
||||||
// },
|
// scalar_error: err,
|
||||||
// (err) => {
|
// });
|
||||||
// this.setState({
|
|
||||||
// scalar_error: err,
|
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -71,6 +72,27 @@ module.exports = React.createClass({
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps(newProps) {
|
||||||
|
// Room has changed probably, update apps
|
||||||
|
this._updateApps();
|
||||||
|
},
|
||||||
|
|
||||||
|
onAction: function(action) {
|
||||||
|
switch (action.action) {
|
||||||
|
case 'appsDrawer':
|
||||||
|
// When opening the app draw when there aren't any apps, auto-launch the
|
||||||
|
// integrations manager to skip the awkward click on "Add widget"
|
||||||
|
if (action.show) {
|
||||||
|
const apps = this._getApps();
|
||||||
|
if (apps.length === 0) {
|
||||||
|
this._launchManageIntegrations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -93,7 +115,7 @@ module.exports = React.createClass({
|
||||||
return pathTemplate;
|
return pathTemplate;
|
||||||
},
|
},
|
||||||
|
|
||||||
_initAppConfig: function(appId, app) {
|
_initAppConfig: function(appId, app, sender) {
|
||||||
const user = MatrixClientPeg.get().getUser(this.props.userId);
|
const user = MatrixClientPeg.get().getUser(this.props.userId);
|
||||||
const params = {
|
const params = {
|
||||||
'$matrix_user_id': this.props.userId,
|
'$matrix_user_id': this.props.userId,
|
||||||
|
@ -111,6 +133,7 @@ module.exports = React.createClass({
|
||||||
app.id = appId;
|
app.id = appId;
|
||||||
app.name = app.name || app.type;
|
app.name = app.name || app.type;
|
||||||
app.url = this.encodeUri(app.url, params);
|
app.url = this.encodeUri(app.url, params);
|
||||||
|
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
},
|
},
|
||||||
|
@ -131,18 +154,12 @@ module.exports = React.createClass({
|
||||||
return appsStateEvents.filter((ev) => {
|
return appsStateEvents.filter((ev) => {
|
||||||
return ev.getContent().type && ev.getContent().url;
|
return ev.getContent().type && ev.getContent().url;
|
||||||
}).map((ev) => {
|
}).map((ev) => {
|
||||||
return this._initAppConfig(ev.getStateKey(), ev.getContent());
|
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateApps: function() {
|
_updateApps: function() {
|
||||||
const apps = this._getApps();
|
const apps = this._getApps();
|
||||||
if (apps.length < 1) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'appsDrawer',
|
|
||||||
show: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.setState({
|
this.setState({
|
||||||
apps: apps,
|
apps: apps,
|
||||||
});
|
});
|
||||||
|
@ -157,11 +174,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onClickAddWidget: function(e) {
|
_launchManageIntegrations: function() {
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
|
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
|
||||||
|
@ -171,6 +184,23 @@ module.exports = React.createClass({
|
||||||
}, "mx_IntegrationsManager");
|
}, "mx_IntegrationsManager");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onClickAddWidget: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Display a warning dialog if the max number of widgets have already been added to the room
|
||||||
|
const apps = this._getApps();
|
||||||
|
if (apps && apps.length >= MAX_WIDGETS) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
const errorMsg = `The maximum number of ${MAX_WIDGETS} widgets have already been added to this room.`;
|
||||||
|
console.error(errorMsg);
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: _t("Cannot add any more widgets"),
|
||||||
|
description: _t("The maximum permitted number of widgets have already been added to this room."),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._launchManageIntegrations();
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const apps = this.state.apps.map(
|
const apps = this.state.apps.map(
|
||||||
(app, index, arr) => {
|
(app, index, arr) => {
|
||||||
|
@ -183,24 +213,34 @@ module.exports = React.createClass({
|
||||||
fullWidth={arr.length<2 ? true : false}
|
fullWidth={arr.length<2 ? true : false}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
|
show={this.props.showApps}
|
||||||
|
creatorUserId={app.creatorUserId}
|
||||||
/>);
|
/>);
|
||||||
});
|
});
|
||||||
|
|
||||||
const addWidget = this.state.apps && this.state.apps.length < 2 && this._canUserModify() &&
|
let addWidget;
|
||||||
(<div onClick={this.onClickAddWidget}
|
if (this.props.showApps &&
|
||||||
role="button"
|
this._canUserModify()
|
||||||
tabIndex="0"
|
) {
|
||||||
className="mx_AddWidget_button"
|
addWidget = <div
|
||||||
title={_t('Add a widget')}>
|
onClick={this.onClickAddWidget}
|
||||||
[+] {_t('Add a widget')}
|
role="button"
|
||||||
</div>);
|
tabIndex="0"
|
||||||
|
className={this.state.apps.length<2 ?
|
||||||
|
"mx_AddWidget_button mx_AddWidget_button_full_width" :
|
||||||
|
"mx_AddWidget_button"
|
||||||
|
}
|
||||||
|
title={_t('Add a widget')}>
|
||||||
|
[+] { _t('Add a widget') }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_AppsDrawer">
|
<div className="mx_AppsDrawer">
|
||||||
<div id="apps" className="mx_AppsContainer">
|
<div id="apps" className="mx_AppsContainer">
|
||||||
{apps}
|
{ apps }
|
||||||
</div>
|
</div>
|
||||||
{addWidget}
|
{ this._canUserModify() && addWidget }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -143,7 +143,6 @@ export default class Autocomplete extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
this.setSelection(selectionOffset);
|
this.setSelection(selectionOffset);
|
||||||
return selectionOffset === COMPOSER_SELECTED ? null : this.state.completionList[selectionOffset - 1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// called from MessageComposerInput
|
// called from MessageComposerInput
|
||||||
|
@ -155,7 +154,6 @@ export default class Autocomplete extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
this.setSelection(selectionOffset);
|
this.setSelection(selectionOffset);
|
||||||
return selectionOffset === COMPOSER_SELECTED ? null : this.state.completionList[selectionOffset - 1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEscape(e): boolean {
|
onEscape(e): boolean {
|
||||||
|
@ -201,6 +199,9 @@ export default class Autocomplete extends React.Component {
|
||||||
|
|
||||||
setSelection(selectionOffset: number) {
|
setSelection(selectionOffset: number) {
|
||||||
this.setState({selectionOffset, hide: false});
|
this.setState({selectionOffset, hide: false});
|
||||||
|
if (this.props.onSelectionChange) {
|
||||||
|
this.props.onSelectionChange(this.state.completionList[selectionOffset - 1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
|
|
@ -129,11 +129,13 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
|
|
||||||
let appsDrawer = null;
|
let appsDrawer = null;
|
||||||
if(UserSettingsStore.isFeatureEnabled('matrix_apps') && this.props.showApps) {
|
if(UserSettingsStore.isFeatureEnabled('matrix_apps')) {
|
||||||
appsDrawer = <AppsDrawer ref="appsDrawer"
|
appsDrawer = <AppsDrawer ref="appsDrawer"
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
maxHeight={this.props.maxHeight}/>;
|
maxHeight={this.props.maxHeight}
|
||||||
|
showApps={this.props.showApps}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -44,6 +44,8 @@ var eventTileTypes = {
|
||||||
'm.room.history_visibility' : 'messages.TextualEvent',
|
'm.room.history_visibility' : 'messages.TextualEvent',
|
||||||
'm.room.encryption' : 'messages.TextualEvent',
|
'm.room.encryption' : 'messages.TextualEvent',
|
||||||
'm.room.power_levels' : 'messages.TextualEvent',
|
'm.room.power_levels' : 'messages.TextualEvent',
|
||||||
|
|
||||||
|
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||||
};
|
};
|
||||||
|
|
||||||
var MAX_READ_AVATARS = 5;
|
var MAX_READ_AVATARS = 5;
|
||||||
|
@ -506,10 +508,10 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
if (msgtype === 'm.image') aux = _t('sent an image');
|
if (msgtype === 'm.image') aux = _t('sent an image');
|
||||||
else if (msgtype === 'm.video') aux = _t('sent a video');
|
else if (msgtype === 'm.video') aux = _t('sent a video');
|
||||||
else if (msgtype === 'm.file') aux = _t('uploaded a file');
|
else if (msgtype === 'm.file') aux = _t('uploaded a file');
|
||||||
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
|
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} enableFlair={!aux} aux={aux} />;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sender = <SenderProfile mxEvent={this.props.mxEvent} />;
|
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={true} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ForwardMessage">
|
<div className="mx_ForwardMessage">
|
||||||
<h1>{_t('Please select the destination room for this message')}</h1>
|
<h1>{ _t('Please select the destination room for this message') }</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -62,6 +62,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
updating: 0,
|
updating: 0,
|
||||||
devicesLoading: true,
|
devicesLoading: true,
|
||||||
devices: null,
|
devices: null,
|
||||||
|
isIgnoring: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -81,6 +82,8 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
cli.on("accountData", this.onAccountData);
|
cli.on("accountData", this.onAccountData);
|
||||||
|
|
||||||
|
this._checkIgnoreState();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -111,6 +114,11 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_checkIgnoreState: function() {
|
||||||
|
const isIgnoring = this.props.matrixClient.isUserIgnored(this.props.member.userId);
|
||||||
|
this.setState({isIgnoring: isIgnoring});
|
||||||
|
},
|
||||||
|
|
||||||
_disambiguateDevices: function(devices) {
|
_disambiguateDevices: function(devices) {
|
||||||
var names = Object.create(null);
|
var names = Object.create(null);
|
||||||
for (var i = 0; i < devices.length; i++) {
|
for (var i = 0; i < devices.length; i++) {
|
||||||
|
@ -225,6 +233,18 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onIgnoreToggle: function() {
|
||||||
|
const ignoredUsers = this.props.matrixClient.getIgnoredUsers();
|
||||||
|
if (this.state.isIgnoring) {
|
||||||
|
const index = ignoredUsers.indexOf(this.props.member.userId);
|
||||||
|
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
ignoredUsers.push(this.props.member.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.matrixClient.setIgnoredUsers(ignoredUsers).then(() => this.setState({isIgnoring: !this.state.isIgnoring}));
|
||||||
|
},
|
||||||
|
|
||||||
onKick: function() {
|
onKick: function() {
|
||||||
const membership = this.props.member.membership;
|
const membership = this.props.member.membership;
|
||||||
const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
|
const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
|
||||||
|
@ -607,6 +627,29 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderUserOptions: function() {
|
||||||
|
// Only allow the user to ignore the user if its not ourselves
|
||||||
|
let ignoreButton = null;
|
||||||
|
if (this.props.member.userId !== this.props.matrixClient.getUserId()) {
|
||||||
|
ignoreButton = (
|
||||||
|
<AccessibleButton onClick={this.onIgnoreToggle} className="mx_MemberInfo_field">
|
||||||
|
{this.state.isIgnoring ? _t("Unignore") : _t("Ignore")}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ignoreButton) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>{ _t("User Options") }</h3>
|
||||||
|
<div className="mx_MemberInfo_buttons">
|
||||||
|
{ignoreButton}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
|
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
|
||||||
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
|
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
|
||||||
|
@ -708,7 +751,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
if (kickButton || banButton || muteButton || giveModButton) {
|
if (kickButton || banButton || muteButton || giveModButton) {
|
||||||
adminTools =
|
adminTools =
|
||||||
<div>
|
<div>
|
||||||
<h3>{_t("Admin tools")}</h3>
|
<h3>{_t("Admin Tools")}</h3>
|
||||||
|
|
||||||
<div className="mx_MemberInfo_buttons">
|
<div className="mx_MemberInfo_buttons">
|
||||||
{muteButton}
|
{muteButton}
|
||||||
|
@ -756,6 +799,8 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{ this._renderUserOptions() }
|
||||||
|
|
||||||
{ adminTools }
|
{ adminTools }
|
||||||
|
|
||||||
{ startChat }
|
{ startChat }
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations 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,42 +15,37 @@ 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.
|
||||||
*/
|
*/
|
||||||
var React = require('react');
|
|
||||||
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
var classNames = require('classnames');
|
|
||||||
var Matrix = require("matrix-js-sdk");
|
|
||||||
import Promise from 'bluebird';
|
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var Modal = require("../../../Modal");
|
|
||||||
var Entities = require("../../../Entities");
|
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
var rate_limited_func = require('../../../ratelimitedfunc');
|
var rate_limited_func = require('../../../ratelimitedfunc');
|
||||||
var CallHandler = require("../../../CallHandler");
|
var CallHandler = require("../../../CallHandler");
|
||||||
var Invite = require("../../../Invite");
|
|
||||||
|
|
||||||
var INITIAL_LOAD_NUM_MEMBERS = 30;
|
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||||
|
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||||
|
const SHOW_MORE_INCREMENT = 100;
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MemberList',
|
displayName: 'MemberList',
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
var state = {
|
this.memberDict = this.getMemberDict();
|
||||||
members: [],
|
const members = this.roomMembers();
|
||||||
|
|
||||||
|
return {
|
||||||
|
members: members,
|
||||||
|
filteredJoinedMembers: this._filterMembers(members, 'join'),
|
||||||
|
filteredInvitedMembers: this._filterMembers(members, 'invite'),
|
||||||
|
|
||||||
// ideally we'd size this to the page height, but
|
// ideally we'd size this to the page height, but
|
||||||
// in practice I find that a little constraining
|
// in practice I find that a little constraining
|
||||||
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
|
truncateAtJoined: INITIAL_LOAD_NUM_MEMBERS,
|
||||||
|
truncateAtInvited: INITIAL_LOAD_NUM_INVITED,
|
||||||
searchQuery: "",
|
searchQuery: "",
|
||||||
};
|
};
|
||||||
if (!this.props.roomId) return state;
|
|
||||||
var cli = MatrixClientPeg.get();
|
|
||||||
var room = cli.getRoom(this.props.roomId);
|
|
||||||
if (!room) return state;
|
|
||||||
|
|
||||||
this.memberDict = this.getMemberDict();
|
|
||||||
|
|
||||||
state.members = this.roomMembers();
|
|
||||||
return state;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -147,10 +143,12 @@ module.exports = React.createClass({
|
||||||
// console.log("Updating memberlist");
|
// console.log("Updating memberlist");
|
||||||
this.memberDict = this.getMemberDict();
|
this.memberDict = this.getMemberDict();
|
||||||
|
|
||||||
var self = this;
|
const newState = {
|
||||||
this.setState({
|
members: this.roomMembers(),
|
||||||
members: self.roomMembers()
|
};
|
||||||
});
|
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join');
|
||||||
|
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite');
|
||||||
|
this.setState(newState);
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
||||||
getMemberDict: function() {
|
getMemberDict: function() {
|
||||||
|
@ -199,7 +197,15 @@ module.exports = React.createClass({
|
||||||
return to_display;
|
return to_display;
|
||||||
},
|
},
|
||||||
|
|
||||||
_createOverflowTile: function(overflowCount, totalCount) {
|
_createOverflowTileJoined: function(overflowCount, totalCount) {
|
||||||
|
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList);
|
||||||
|
},
|
||||||
|
|
||||||
|
_createOverflowTileInvited: function(overflowCount, totalCount) {
|
||||||
|
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList);
|
||||||
|
},
|
||||||
|
|
||||||
|
_createOverflowTile: function(overflowCount, totalCount, onClick) {
|
||||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
|
@ -208,13 +214,19 @@ module.exports = React.createClass({
|
||||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||||
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
||||||
} name={text} presenceState="online" suppressOnHover={true}
|
} name={text} presenceState="online" suppressOnHover={true}
|
||||||
onClick={this._showFullMemberList} />
|
onClick={onClick} />
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_showFullMemberList: function() {
|
_showMoreJoinedMemberList: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
truncateAt: -1
|
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_showMoreInvitedMemberList: function() {
|
||||||
|
this.setState({
|
||||||
|
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -280,17 +292,17 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchQueryChanged: function(ev) {
|
onSearchQueryChanged: function(ev) {
|
||||||
this.setState({ searchQuery: ev.target.value });
|
const q = ev.target.value;
|
||||||
|
this.setState({
|
||||||
|
searchQuery: q,
|
||||||
|
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', q),
|
||||||
|
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', q),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
makeMemberTiles: function(membership, query) {
|
_filterMembers: function(members, membership, query) {
|
||||||
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
return members.filter((userId) => {
|
||||||
query = (query || "").toLowerCase();
|
const m = this.memberDict[userId];
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var memberList = self.state.members.filter(function(userId) {
|
|
||||||
var m = self.memberDict[userId];
|
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
|
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
|
||||||
|
@ -302,14 +314,23 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.membership == membership;
|
return m.membership == membership;
|
||||||
}).map(function(userId) {
|
});
|
||||||
var m = self.memberDict[userId];
|
},
|
||||||
|
|
||||||
|
_makeMemberTiles: function(members, membership) {
|
||||||
|
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||||
|
|
||||||
|
const memberList = members.map((userId) => {
|
||||||
|
const m = this.memberDict[userId];
|
||||||
return (
|
return (
|
||||||
<MemberTile key={userId} member={m} ref={userId} />
|
<MemberTile key={userId} member={m} ref={userId} />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// XXX: surely this is not the right home for this logic.
|
// XXX: surely this is not the right home for this logic.
|
||||||
|
// Double XXX: Now it's really, really not the right home for this logic:
|
||||||
|
// we shouldn't even be passing in the 'membership' param to this function.
|
||||||
|
// Ew, ew, and ew.
|
||||||
if (membership === "invite") {
|
if (membership === "invite") {
|
||||||
// include 3pid invites (m.room.third_party_invite) state events.
|
// include 3pid invites (m.room.third_party_invite) state events.
|
||||||
// The HS may have already converted these into m.room.member invites so
|
// The HS may have already converted these into m.room.member invites so
|
||||||
|
@ -333,7 +354,7 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
memberList.push(
|
memberList.push(
|
||||||
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} />
|
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} suppressOnHover={true} />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -342,21 +363,42 @@ module.exports = React.createClass({
|
||||||
return memberList;
|
return memberList;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_getChildrenJoined: function(start, end) {
|
||||||
|
return this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
|
||||||
|
},
|
||||||
|
|
||||||
|
_getChildCountJoined: function() {
|
||||||
|
return this.state.filteredJoinedMembers.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getChildrenInvited: function(start, end) {
|
||||||
|
return this._makeMemberTiles(this.state.filteredInvitedMembers.slice(start, end), 'invite');
|
||||||
|
},
|
||||||
|
|
||||||
|
_getChildCountInvited: function() {
|
||||||
|
return this.state.filteredInvitedMembers.length;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var invitedSection = null;
|
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||||
var invitedMemberTiles = this.makeMemberTiles('invite', this.state.searchQuery);
|
|
||||||
if (invitedMemberTiles.length > 0) {
|
let invitedSection = null;
|
||||||
|
if (this._getChildCountInvited() > 0) {
|
||||||
invitedSection = (
|
invitedSection = (
|
||||||
<div className="mx_MemberList_invited">
|
<div className="mx_MemberList_invited">
|
||||||
<h2>{ _t("Invited") }</h2>
|
<h2>{ _t("Invited") }</h2>
|
||||||
<div className="mx_MemberList_wrapper">
|
<div className="mx_MemberList_wrapper">
|
||||||
{invitedMemberTiles}
|
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtInvited}
|
||||||
|
createOverflowElement={this._createOverflowTileInvited}
|
||||||
|
getChildren={this._getChildrenInvited}
|
||||||
|
getChildCount={this._getChildCountInvited}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var inputBox = (
|
const inputBox = (
|
||||||
<form autoComplete="off">
|
<form autoComplete="off">
|
||||||
<input className="mx_MemberList_query" id="mx_MemberList_query" type="text"
|
<input className="mx_MemberList_query" id="mx_MemberList_query" type="text"
|
||||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||||
|
@ -364,15 +406,15 @@ module.exports = React.createClass({
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
||||||
var TruncatedList = sdk.getComponent("elements.TruncatedList");
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberList">
|
<div className="mx_MemberList">
|
||||||
{ inputBox }
|
{ inputBox }
|
||||||
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
|
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
|
||||||
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
|
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtJoined}
|
||||||
createOverflowElement={this._createOverflowTile}>
|
createOverflowElement={this._createOverflowTileJoined}
|
||||||
{this.makeMemberTiles('join', this.state.searchQuery)}
|
getChildren={this._getChildrenJoined}
|
||||||
</TruncatedList>
|
getChildCount={this._getChildCountJoined}
|
||||||
|
/>
|
||||||
{invitedSection}
|
{invitedSection}
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -289,12 +289,12 @@ export default class MessageComposer extends React.Component {
|
||||||
if (this.props.showApps) {
|
if (this.props.showApps) {
|
||||||
hideAppsButton =
|
hideAppsButton =
|
||||||
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
|
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
|
||||||
<TintableSvg src="img/icons-apps-active.svg" width="35" height="35"/>
|
<TintableSvg src="img/icons-hide-apps.svg" width="35" height="35"/>
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
showAppsButton =
|
showAppsButton =
|
||||||
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
|
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
|
||||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
|
<TintableSvg src="img/icons-show-apps.svg" width="35" height="35"/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import SlashCommands from '../../../SlashCommands';
|
||||||
import KeyCode from '../../../KeyCode';
|
import KeyCode from '../../../KeyCode';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import Analytics from '../../../Analytics';
|
import Analytics from '../../../Analytics';
|
||||||
|
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
@ -949,8 +949,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
moveAutocompleteSelection = (up) => {
|
moveAutocompleteSelection = (up) => {
|
||||||
const completion = up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
|
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
|
||||||
return this.setDisplayedCompletion(completion);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onEscape = async (e) => {
|
onEscape = async (e) => {
|
||||||
|
@ -1033,10 +1032,10 @@ export default class MessageComposerInput extends React.Component {
|
||||||
buttons. */
|
buttons. */
|
||||||
getSelectionInfo(editorState: EditorState) {
|
getSelectionInfo(editorState: EditorState) {
|
||||||
const styleName = {
|
const styleName = {
|
||||||
BOLD: 'bold',
|
BOLD: _td('bold'),
|
||||||
ITALIC: 'italic',
|
ITALIC: _td('italic'),
|
||||||
STRIKETHROUGH: 'strike',
|
STRIKETHROUGH: _td('strike'),
|
||||||
UNDERLINE: 'underline',
|
UNDERLINE: _td('underline'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalStyle = editorState.getCurrentInlineStyle().toArray();
|
const originalStyle = editorState.getCurrentInlineStyle().toArray();
|
||||||
|
@ -1045,10 +1044,10 @@ export default class MessageComposerInput extends React.Component {
|
||||||
.filter((styleName) => !!styleName);
|
.filter((styleName) => !!styleName);
|
||||||
|
|
||||||
const blockName = {
|
const blockName = {
|
||||||
'code-block': 'code',
|
'code-block': _td('code'),
|
||||||
'blockquote': 'quote',
|
'blockquote': _td('quote'),
|
||||||
'unordered-list-item': 'bullet',
|
'unordered-list-item': _td('bullet'),
|
||||||
'ordered-list-item': 'numbullet',
|
'ordered-list-item': _td('numbullet'),
|
||||||
};
|
};
|
||||||
const originalBlockType = editorState.getCurrentContent()
|
const originalBlockType = editorState.getCurrentContent()
|
||||||
.getBlockForKey(editorState.getSelection().getStartKey())
|
.getBlockForKey(editorState.getSelection().getStartKey())
|
||||||
|
@ -1133,6 +1132,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
ref={(e) => this.autocomplete = e}
|
ref={(e) => this.autocomplete = e}
|
||||||
onConfirm={this.setDisplayedCompletion}
|
onConfirm={this.setDisplayedCompletion}
|
||||||
|
onSelectionChange={this.setDisplayedCompletion}
|
||||||
query={this.getAutocompleteQuery(content)}
|
query={this.getAutocompleteQuery(content)}
|
||||||
selection={selection}/>
|
selection={selection}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,7 +70,7 @@ module.exports = React.createClass({
|
||||||
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 "Unknown";
|
return _t("Unknown");
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
|
|
@ -123,7 +123,19 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
var newElement = ReactDOM.findDOMNode(this);
|
var newElement = ReactDOM.findDOMNode(this);
|
||||||
var startTopOffset = oldTop - newElement.offsetParent.getBoundingClientRect().top;
|
let startTopOffset;
|
||||||
|
if (!newElement.offsetParent) {
|
||||||
|
// this seems to happen sometimes for reasons I don't understand
|
||||||
|
// the docs for `offsetParent` say it may be null if `display` is
|
||||||
|
// `none`, but I can't see why that would happen.
|
||||||
|
console.warn(
|
||||||
|
`ReadReceiptMarker for ${this.props.member.userId} in ` +
|
||||||
|
`${this.props.member.roomId} has no offsetParent`,
|
||||||
|
);
|
||||||
|
startTopOffset = 0;
|
||||||
|
} else {
|
||||||
|
startTopOffset = oldTop - newElement.offsetParent.getBoundingClientRect().top;
|
||||||
|
}
|
||||||
|
|
||||||
var startStyles = [];
|
var startStyles = [];
|
||||||
var enterTransitionOpts = [];
|
var enterTransitionOpts = [];
|
||||||
|
@ -131,13 +143,12 @@ module.exports = React.createClass({
|
||||||
if (oldInfo && oldInfo.left) {
|
if (oldInfo && oldInfo.left) {
|
||||||
// start at the old height and in the old h pos
|
// start at the old height and in the old h pos
|
||||||
|
|
||||||
var leftOffset = oldInfo.left;
|
|
||||||
startStyles.push({ top: startTopOffset+"px",
|
startStyles.push({ top: startTopOffset+"px",
|
||||||
left: oldInfo.left+"px" });
|
left: oldInfo.left+"px" });
|
||||||
|
|
||||||
var reorderTransitionOpts = {
|
var reorderTransitionOpts = {
|
||||||
duration: 100,
|
duration: 100,
|
||||||
easing: 'easeOut'
|
easing: 'easeOut',
|
||||||
};
|
};
|
||||||
|
|
||||||
enterTransitionOpts.push(reorderTransitionOpts);
|
enterTransitionOpts.push(reorderTransitionOpts);
|
||||||
|
@ -175,7 +186,7 @@ module.exports = React.createClass({
|
||||||
if (this.props.timestamp) {
|
if (this.props.timestamp) {
|
||||||
title = _t(
|
title = _t(
|
||||||
"Seen by %(userName)s at %(dateTime)s",
|
"Seen by %(userName)s at %(dateTime)s",
|
||||||
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp), this.props.showTwelveHour)}
|
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp), this.props.showTwelveHour)},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import * as linkify from 'linkifyjs';
|
||||||
import linkifyElement from 'linkifyjs/element';
|
import linkifyElement from 'linkifyjs/element';
|
||||||
import linkifyMatrix from '../../../linkify-matrix';
|
import linkifyMatrix from '../../../linkify-matrix';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import ManageIntegsButton from '../elements/ManageIntegsButton';
|
||||||
import {CancelButton} from './SimpleRoomHeader';
|
import {CancelButton} from './SimpleRoomHeader';
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
@ -47,6 +48,7 @@ module.exports = React.createClass({
|
||||||
onSaveClick: React.PropTypes.func,
|
onSaveClick: React.PropTypes.func,
|
||||||
onSearchClick: React.PropTypes.func,
|
onSearchClick: React.PropTypes.func,
|
||||||
onLeaveClick: React.PropTypes.func,
|
onLeaveClick: React.PropTypes.func,
|
||||||
|
onCancelClick: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -54,6 +56,7 @@ module.exports = React.createClass({
|
||||||
editing: false,
|
editing: false,
|
||||||
inRoom: false,
|
inRoom: false,
|
||||||
onSaveClick: function() {},
|
onSaveClick: function() {},
|
||||||
|
onCancelClick: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -183,18 +186,18 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
saveButton = (
|
saveButton = (
|
||||||
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
|
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
|
||||||
{_t("Save")}
|
{ _t("Save") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
cancelButton = <CancelButton onClick={this.props.onCancelClick}/>;
|
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.saving) {
|
if (this.props.saving) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
|
spinner = <div className="mx_RoomHeader_spinner"><Spinner /></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canSetRoomName) {
|
if (canSetRoomName) {
|
||||||
|
@ -251,7 +254,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
if (topic) {
|
if (topic) {
|
||||||
topicElement =
|
topicElement =
|
||||||
<div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
|
<div className="mx_RoomHeader_topic" ref="topic" title={topic} dir="auto">{ topic }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,16 +262,16 @@ module.exports = React.createClass({
|
||||||
if (canSetRoomAvatar) {
|
if (canSetRoomAvatar) {
|
||||||
roomAvatar = (
|
roomAvatar = (
|
||||||
<div className="mx_RoomHeader_avatarPicker">
|
<div className="mx_RoomHeader_avatarPicker">
|
||||||
<div onClick={ this.onAvatarPickerClick }>
|
<div onClick={this.onAvatarPickerClick}>
|
||||||
<ChangeAvatar ref="changeAvatar" room={this.props.room} showUploadSection={false} width={48} height={48} />
|
<ChangeAvatar ref="changeAvatar" room={this.props.room} showUploadSection={false} width={48} height={48} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomHeader_avatarPicker_edit">
|
<div className="mx_RoomHeader_avatarPicker_edit">
|
||||||
<label htmlFor="avatarInput" ref="file_label">
|
<label htmlFor="avatarInput" ref="file_label">
|
||||||
<img src="img/camera.svg"
|
<img src="img/camera.svg"
|
||||||
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
|
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
||||||
width="17" height="15" />
|
width="17" height="15" />
|
||||||
</label>
|
</label>
|
||||||
<input id="avatarInput" type="file" onChange={ this.onAvatarSelected }/>
|
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -283,7 +286,7 @@ module.exports = React.createClass({
|
||||||
if (this.props.onSettingsClick) {
|
if (this.props.onSettingsClick) {
|
||||||
settingsButton =
|
settingsButton =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
|
||||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16" />
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,32 +301,40 @@ module.exports = React.createClass({
|
||||||
let forgetButton;
|
let forgetButton;
|
||||||
if (this.props.onForgetClick) {
|
if (this.props.onForgetClick) {
|
||||||
forgetButton =
|
forgetButton =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={_t("Forget room")}>
|
||||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
<TintableSvg src="img/leave.svg" width="26" height="20" />
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let searchButton;
|
let searchButton;
|
||||||
if (this.props.onSearchClick && this.props.inRoom) {
|
if (this.props.onSearchClick && this.props.inRoom) {
|
||||||
searchButton =
|
searchButton =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={_t("Search")}>
|
||||||
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
<TintableSvg src="img/icons-search.svg" width="35" height="35" />
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rightPanelButtons;
|
let rightPanelButtons;
|
||||||
if (this.props.collapsedRhs) {
|
if (this.props.collapsedRhs) {
|
||||||
rightPanelButtons =
|
rightPanelButtons =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={_t('Show panel')}>
|
||||||
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
|
<TintableSvg src="img/maximise.svg" width="10" height="16" />
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rightRow;
|
let rightRow;
|
||||||
|
let manageIntegsButton;
|
||||||
|
if(this.props.room && this.props.room.roomId && this.props.inRoom) {
|
||||||
|
manageIntegsButton = <ManageIntegsButton
|
||||||
|
roomId={this.props.room.roomId}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.props.editing) {
|
if (!this.props.editing) {
|
||||||
rightRow =
|
rightRow =
|
||||||
<div className="mx_RoomHeader_rightRow">
|
<div className="mx_RoomHeader_rightRow">
|
||||||
{ settingsButton }
|
{ settingsButton }
|
||||||
|
{ manageIntegsButton }
|
||||||
{ forgetButton }
|
{ forgetButton }
|
||||||
{ searchButton }
|
{ searchButton }
|
||||||
{ rightPanelButtons }
|
{ rightPanelButtons }
|
||||||
|
@ -331,7 +342,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
|
<div className={"mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "")}>
|
||||||
<div className="mx_RoomHeader_wrapper">
|
<div className="mx_RoomHeader_wrapper">
|
||||||
<div className="mx_RoomHeader_leftRow">
|
<div className="mx_RoomHeader_leftRow">
|
||||||
<div className="mx_RoomHeader_avatar">
|
<div className="mx_RoomHeader_avatar">
|
||||||
|
@ -342,10 +353,10 @@ module.exports = React.createClass({
|
||||||
{ topicElement }
|
{ topicElement }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{spinner}
|
{ spinner }
|
||||||
{saveButton}
|
{ saveButton }
|
||||||
{cancelButton}
|
{ cancelButton }
|
||||||
{rightRow}
|
{ rightRow }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,7 +63,6 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
ConferenceHandler: React.PropTypes.any,
|
ConferenceHandler: React.PropTypes.any,
|
||||||
collapsed: React.PropTypes.bool.isRequired,
|
collapsed: React.PropTypes.bool.isRequired,
|
||||||
currentRoom: React.PropTypes.string,
|
|
||||||
searchFilter: React.PropTypes.string,
|
searchFilter: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -88,7 +87,9 @@ module.exports = React.createClass({
|
||||||
cli.on("Room.receipt", this.onRoomReceipt);
|
cli.on("Room.receipt", this.onRoomReceipt);
|
||||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
|
cli.on("Event.decrypted", this.onEventDecrypted);
|
||||||
cli.on("accountData", this.onAccountData);
|
cli.on("accountData", this.onAccountData);
|
||||||
|
cli.on("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
|
||||||
this.refreshRoomList();
|
this.refreshRoomList();
|
||||||
|
|
||||||
|
@ -155,7 +156,9 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||||
|
MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted);
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
|
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
}
|
}
|
||||||
// cancel any pending calls to the rate_limited_funcs
|
// cancel any pending calls to the rate_limited_funcs
|
||||||
this._delayedRefreshRoomList.cancelPendingCall();
|
this._delayedRefreshRoomList.cancelPendingCall();
|
||||||
|
@ -224,12 +227,21 @@ module.exports = React.createClass({
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onEventDecrypted: function(ev) {
|
||||||
|
// An event being decrypted may mean we need to re-order the room list
|
||||||
|
this._delayedRefreshRoomList();
|
||||||
|
},
|
||||||
|
|
||||||
onAccountData: function(ev) {
|
onAccountData: function(ev) {
|
||||||
if (ev.getType() == 'm.direct') {
|
if (ev.getType() == 'm.direct') {
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onGroupMyMembership: function(group) {
|
||||||
|
this.forceUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
_delayedRefreshRoomList: new rate_limited_func(function() {
|
_delayedRefreshRoomList: new rate_limited_func(function() {
|
||||||
this.refreshRoomList();
|
this.refreshRoomList();
|
||||||
}, 500),
|
}, 500),
|
||||||
|
@ -544,8 +556,24 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_makeGroupInviteTiles() {
|
||||||
|
const ret = [];
|
||||||
|
|
||||||
|
const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile');
|
||||||
|
for (const group of MatrixClientPeg.get().getGroups()) {
|
||||||
|
if (group.myMembership !== 'invite') continue;
|
||||||
|
|
||||||
|
ret.push(<GroupInviteTile key={group.groupId} group={group} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var RoomSubList = sdk.getComponent('structures.RoomSubList');
|
const RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||||
|
|
||||||
|
const inviteSectionExtraTiles = this._makeGroupInviteTiles();
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
return (
|
return (
|
||||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||||
|
@ -555,12 +583,15 @@ module.exports = React.createClass({
|
||||||
label={ _t('Invites') }
|
label={ _t('Invites') }
|
||||||
editable={ false }
|
editable={ false }
|
||||||
order="recent"
|
order="recent"
|
||||||
|
isInvite={true}
|
||||||
selectedRoom={ self.props.selectedRoom }
|
selectedRoom={ self.props.selectedRoom }
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms }
|
||||||
|
extraTiles={ inviteSectionExtraTiles }
|
||||||
|
/>
|
||||||
|
|
||||||
<RoomSubList list={ self.state.lists['m.favourite'] }
|
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||||
label={ _t('Favourites') }
|
label={ _t('Favourites') }
|
||||||
|
|
|
@ -24,8 +24,6 @@ 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 ScalarAuthClient from '../../../ScalarAuthClient';
|
|
||||||
import ScalarMessaging from '../../../ScalarMessaging';
|
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
|
@ -92,7 +90,6 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
onSaveClick: React.PropTypes.func,
|
onSaveClick: React.PropTypes.func,
|
||||||
onCancelClick: React.PropTypes.func,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -118,14 +115,10 @@ module.exports = React.createClass({
|
||||||
// Default to false if it's undefined, otherwise react complains about changing
|
// Default to false if it's undefined, otherwise react complains about changing
|
||||||
// components from uncontrolled to controlled
|
// components from uncontrolled to controlled
|
||||||
isRoomPublished: this._originalIsRoomPublished || false,
|
isRoomPublished: this._originalIsRoomPublished || false,
|
||||||
scalar_error: null,
|
|
||||||
showIntegrationsError: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
ScalarMessaging.startListening();
|
|
||||||
|
|
||||||
MatrixClientPeg.get().on("RoomMember.membership", this._onRoomMemberMembership);
|
MatrixClientPeg.get().on("RoomMember.membership", this._onRoomMemberMembership);
|
||||||
|
|
||||||
MatrixClientPeg.get().getRoomDirectoryVisibility(
|
MatrixClientPeg.get().getRoomDirectoryVisibility(
|
||||||
|
@ -137,18 +130,6 @@ module.exports = React.createClass({
|
||||||
console.error("Failed to get room visibility: " + err);
|
console.error("Failed to get room visibility: " + err);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scalarClient = null;
|
|
||||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
|
||||||
this.scalarClient = new ScalarAuthClient();
|
|
||||||
this.scalarClient.connect().done(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
}, (err) => {
|
|
||||||
this.setState({
|
|
||||||
scalar_error: err
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'ui_opacity',
|
action: 'ui_opacity',
|
||||||
sideOpacity: 0.3,
|
sideOpacity: 0.3,
|
||||||
|
@ -157,8 +138,6 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
ScalarMessaging.stopListening();
|
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomMember.membership", this._onRoomMemberMembership);
|
cli.removeListener("RoomMember.membership", this._onRoomMemberMembership);
|
||||||
|
@ -308,6 +287,9 @@ module.exports = React.createClass({
|
||||||
promises.push(ps);
|
promises.push(ps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// related groups
|
||||||
|
promises.push(this.saveRelatedGroups());
|
||||||
|
|
||||||
// encryption
|
// encryption
|
||||||
p = this.saveEnableEncryption();
|
p = this.saveEnableEncryption();
|
||||||
if (!p.isFulfilled()) {
|
if (!p.isFulfilled()) {
|
||||||
|
@ -325,6 +307,11 @@ module.exports = React.createClass({
|
||||||
return this.refs.alias_settings.saveSettings();
|
return this.refs.alias_settings.saveSettings();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
saveRelatedGroups: function() {
|
||||||
|
if (!this.refs.related_groups) { return Promise.resolve(); }
|
||||||
|
return this.refs.related_groups.saveSettings();
|
||||||
|
},
|
||||||
|
|
||||||
saveColor: function() {
|
saveColor: function() {
|
||||||
if (!this.refs.color_settings) { return Promise.resolve(); }
|
if (!this.refs.color_settings) { return Promise.resolve(); }
|
||||||
return this.refs.color_settings.saveSettings();
|
return this.refs.color_settings.saveSettings();
|
||||||
|
@ -514,28 +501,6 @@ module.exports = React.createClass({
|
||||||
roomState.mayClientSendStateEvent("m.room.guest_access", cli));
|
roomState.mayClientSendStateEvent("m.room.guest_access", cli));
|
||||||
},
|
},
|
||||||
|
|
||||||
onManageIntegrations(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
|
||||||
Modal.createTrackedDialog('Integrations Manager', 'onManageIntegrations', IntegrationsManager, {
|
|
||||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
|
||||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
|
||||||
null,
|
|
||||||
onFinished: ()=>{
|
|
||||||
if (this._calcSavePromises().length === 0) {
|
|
||||||
this.props.onCancelClick(ev);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}, "mx_IntegrationsManager");
|
|
||||||
},
|
|
||||||
|
|
||||||
onShowIntegrationsError(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.setState({
|
|
||||||
showIntegrationsError: !this.state.showIntegrationsError,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onLeaveClick() {
|
onLeaveClick() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'leave_room',
|
action: 'leave_room',
|
||||||
|
@ -634,6 +599,7 @@ module.exports = React.createClass({
|
||||||
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
||||||
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
|
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
|
||||||
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||||
|
var RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
||||||
var EditableText = sdk.getComponent('elements.EditableText');
|
var EditableText = sdk.getComponent('elements.EditableText');
|
||||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
@ -666,6 +632,14 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
let relatedGroupsSection;
|
||||||
|
if (UserSettingsStore.isFeatureEnabled('feature_groups')) {
|
||||||
|
relatedGroupsSection = <RelatedGroupSettings ref="related_groups"
|
||||||
|
roomId={this.props.room.roomId}
|
||||||
|
canSetRelatedGroups={roomState.mayClientSendStateEvent("m.room.related_groups", cli)}
|
||||||
|
relatedGroupsEvent={this.props.room.currentState.getStateEvents('m.room.related_groups', '')} />;
|
||||||
|
}
|
||||||
|
|
||||||
var userLevelsSection;
|
var userLevelsSection;
|
||||||
if (Object.keys(user_levels).length) {
|
if (Object.keys(user_levels).length) {
|
||||||
userLevelsSection =
|
userLevelsSection =
|
||||||
|
@ -797,46 +771,10 @@ module.exports = React.createClass({
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let integrationsButton;
|
|
||||||
let integrationsError;
|
|
||||||
|
|
||||||
if (this.scalarClient !== null) {
|
|
||||||
if (this.state.showIntegrationsError && this.state.scalar_error) {
|
|
||||||
console.error(this.state.scalar_error);
|
|
||||||
integrationsError = (
|
|
||||||
<span className="mx_RoomSettings_integrationsButton_errorPopup">
|
|
||||||
{ _t('Could not connect to the integration server') }
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.scalarClient.hasCredentials()) {
|
|
||||||
integrationsButton = (
|
|
||||||
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
|
|
||||||
{ _t('Manage Integrations') }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (this.state.scalar_error) {
|
|
||||||
integrationsButton = (
|
|
||||||
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
|
|
||||||
Integrations Error <img src="img/warning.svg" width="17"/>
|
|
||||||
{ integrationsError }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
integrationsButton = (
|
|
||||||
<div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
|
|
||||||
{ _t('Manage Integrations') }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomSettings">
|
<div className="mx_RoomSettings">
|
||||||
|
|
||||||
{ leaveButton }
|
{ leaveButton }
|
||||||
{ integrationsButton }
|
|
||||||
|
|
||||||
{ tagsSection }
|
{ tagsSection }
|
||||||
|
|
||||||
|
@ -872,7 +810,7 @@ module.exports = React.createClass({
|
||||||
<input type="checkbox" disabled={ !roomState.mayClientSendStateEvent("m.room.aliases", cli) }
|
<input type="checkbox" disabled={ !roomState.mayClientSendStateEvent("m.room.aliases", cli) }
|
||||||
onChange={ this._onToggle.bind(this, "isRoomPublished", true, false)}
|
onChange={ this._onToggle.bind(this, "isRoomPublished", true, false)}
|
||||||
checked={this.state.isRoomPublished}/>
|
checked={this.state.isRoomPublished}/>
|
||||||
{_t("List this room in %(domain)s's room directory?", { domain: MatrixClientPeg.get().getDomain() })}
|
{_t("Publish this room to the public in %(domain)s's room directory?", { domain: MatrixClientPeg.get().getDomain() })}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomSettings_settings">
|
<div className="mx_RoomSettings_settings">
|
||||||
|
@ -926,6 +864,8 @@ module.exports = React.createClass({
|
||||||
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
|
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
|
||||||
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
|
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
|
||||||
|
|
||||||
|
{ relatedGroupsSection }
|
||||||
|
|
||||||
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
|
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
|
||||||
|
|
||||||
<h3>{ _t('Permissions') }</h3>
|
<h3>{ _t('Permissions') }</h3>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 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.
|
||||||
|
@ -27,6 +28,8 @@ var RoomNotifs = require('../../../RoomNotifs');
|
||||||
var FormattingUtils = require('../../../utils/FormattingUtils');
|
var FormattingUtils = require('../../../utils/FormattingUtils');
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||||
|
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||||
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomTile',
|
displayName: 'RoomTile',
|
||||||
|
@ -39,7 +42,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
collapsed: React.PropTypes.bool.isRequired,
|
collapsed: React.PropTypes.bool.isRequired,
|
||||||
selected: React.PropTypes.bool.isRequired,
|
|
||||||
unread: React.PropTypes.bool.isRequired,
|
unread: React.PropTypes.bool.isRequired,
|
||||||
highlight: React.PropTypes.bool.isRequired,
|
highlight: React.PropTypes.bool.isRequired,
|
||||||
isInvite: React.PropTypes.bool.isRequired,
|
isInvite: React.PropTypes.bool.isRequired,
|
||||||
|
@ -58,6 +60,7 @@ module.exports = React.createClass({
|
||||||
badgeHover : false,
|
badgeHover : false,
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||||
|
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -87,8 +90,15 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onActiveRoomChange: function() {
|
||||||
|
this.setState({
|
||||||
|
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||||
|
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -96,6 +106,7 @@ module.exports = React.createClass({
|
||||||
if (cli) {
|
if (cli) {
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
}
|
}
|
||||||
|
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function(ev) {
|
onClick: function(ev) {
|
||||||
|
@ -174,7 +185,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var classes = classNames({
|
var classes = classNames({
|
||||||
'mx_RoomTile': true,
|
'mx_RoomTile': true,
|
||||||
'mx_RoomTile_selected': this.props.selected,
|
'mx_RoomTile_selected': this.state.selected,
|
||||||
'mx_RoomTile_unread': this.props.unread,
|
'mx_RoomTile_unread': this.props.unread,
|
||||||
'mx_RoomTile_unreadNotify': notifBadges,
|
'mx_RoomTile_unreadNotify': notifBadges,
|
||||||
'mx_RoomTile_highlight': mentionBadges,
|
'mx_RoomTile_highlight': mentionBadges,
|
||||||
|
@ -221,7 +232,7 @@ module.exports = React.createClass({
|
||||||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.props.selected) {
|
if (this.state.selected) {
|
||||||
let nameSelected = <EmojiText>{name}</EmojiText>;
|
let nameSelected = <EmojiText>{name}</EmojiText>;
|
||||||
|
|
||||||
label = <div title={ name } className={ nameClasses } dir="auto">{ nameSelected }</div>;
|
label = <div title={ name } className={ nameClasses } dir="auto">{ nameSelected }</div>;
|
||||||
|
|
|
@ -26,7 +26,7 @@ export function CancelButton(props) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
|
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
|
||||||
<img src="img/cancel.svg" className='mx_filterFlipColor'
|
<img src="img/cancel.svg" className='mx_filterFlipColor'
|
||||||
width="18" height="18" alt={_t("Cancel")}/>
|
width="18" height="18" alt={_t("Cancel")} />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -208,7 +208,7 @@ module.exports = React.createClass({
|
||||||
if (!this.state.cachedPassword) {
|
if (!this.state.cachedPassword) {
|
||||||
currentPassword = <div className={rowClassName}>
|
currentPassword = <div className={rowClassName}>
|
||||||
<div className={rowLabelClassName}>
|
<div className={rowLabelClassName}>
|
||||||
<label htmlFor="passwordold">Current password</label>
|
<label htmlFor="passwordold">{ _t('Current password') }</label>
|
||||||
</div>
|
</div>
|
||||||
<div className={rowInputClassName}>
|
<div className={rowInputClassName}>
|
||||||
<input id="passwordold" type="password" ref="old_input" />
|
<input id="passwordold" type="password" ref="old_input" />
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default class DevicesPanelEntry extends React.Component {
|
||||||
// pop up an interactive auth dialog
|
// pop up an interactive auth dialog
|
||||||
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||||
|
|
||||||
Modal.createTrackedDialog('Delete Device Dialog', InteractiveAuthDialog, {
|
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
|
||||||
title: _t("Authentication"),
|
title: _t("Authentication"),
|
||||||
matrixClient: MatrixClientPeg.get(),
|
matrixClient: MatrixClientPeg.get(),
|
||||||
authData: error.data,
|
authData: error.data,
|
||||||
|
|
97
src/components/views/voip/CallPreview.js
Normal file
97
src/components/views/voip/CallPreview.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
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 RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
import CallHandler from '../../../CallHandler';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'CallPreview',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// A Conference Handler implementation
|
||||||
|
// Must have a function signature:
|
||||||
|
// getConferenceCallForRoom(roomId: string): MatrixCall
|
||||||
|
ConferenceHandler: React.PropTypes.object,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
roomId: RoomViewStore.getRoomId(),
|
||||||
|
activeCall: CallHandler.getAnyActiveCall(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||||
|
this.dispatcherRef = dis.register(this._onAction);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
if (this._roomStoreToken) {
|
||||||
|
this._roomStoreToken.remove();
|
||||||
|
}
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRoomViewStoreUpdate: function(payload) {
|
||||||
|
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||||
|
this.setState({
|
||||||
|
roomId: RoomViewStore.getRoomId(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onAction: function(payload) {
|
||||||
|
switch (payload.action) {
|
||||||
|
// listen for call state changes to prod the render method, which
|
||||||
|
// may hide the global CallView if the call it is tracking is dead
|
||||||
|
case 'call_state':
|
||||||
|
this.setState({
|
||||||
|
activeCall: CallHandler.getAnyActiveCall(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCallViewClick: function() {
|
||||||
|
const call = CallHandler.getAnyActiveCall();
|
||||||
|
if (call) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: call.groupRoomId || call.roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
|
||||||
|
const showCall = (this.state.activeCall && this.state.activeCall.call_state === 'connected' && !callForRoom);
|
||||||
|
|
||||||
|
if (showCall) {
|
||||||
|
const CallView = sdk.getComponent('voip.CallView');
|
||||||
|
return (
|
||||||
|
<CallView
|
||||||
|
className="mx_LeftPanel_callView" showVoice={true} onClick={this._onCallViewClick}
|
||||||
|
ConferenceHandler={this.props.ConferenceHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
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