mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 18:55:58 +03:00
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/DuckDuckGoProvider.js
|
||||
src/autocomplete/EmojiProvider.js
|
||||
src/autocomplete/RoomProvider.js
|
||||
src/autocomplete/UserProvider.js
|
||||
src/CallHandler.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/DeactivateAccountDialog.js
|
||||
src/components/views/dialogs/InteractiveAuthDialog.js
|
||||
src/components/views/dialogs/SetMxIdDialog.js
|
||||
src/components/views/dialogs/UnknownDeviceDialog.js
|
||||
src/components/views/elements/AccessibleButton.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/StartChatButton.js
|
||||
src/components/views/elements/TintableSvg.js
|
||||
src/components/views/elements/TruncatedList.js
|
||||
src/components/views/elements/UserSelector.js
|
||||
src/components/views/login/CaptchaForm.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/MessageComposer.js
|
||||
src/components/views/rooms/MessageComposerInput.js
|
||||
src/components/views/rooms/MessageComposerInputOld.js
|
||||
src/components/views/rooms/PresenceLabel.js
|
||||
src/components/views/rooms/ReadReceiptMarker.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/SearchableEntityList.js
|
||||
src/components/views/rooms/SearchResultTile.js
|
||||
src/components/views/rooms/TabCompleteBar.js
|
||||
src/components/views/rooms/TopUnreadMessagesBar.js
|
||||
src/components/views/rooms/UserTile.js
|
||||
src/components/views/settings/AddPhoneNumber.js
|
||||
|
@ -128,9 +123,6 @@ src/Roles.js
|
|||
src/Rooms.js
|
||||
src/ScalarAuthClient.js
|
||||
src/ScalarMessaging.js
|
||||
src/TabComplete.js
|
||||
src/TabCompleteEntries.js
|
||||
src/TextForEvent.js
|
||||
src/Tinter.js
|
||||
src/UiEffects.js
|
||||
src/Unread.js
|
||||
|
@ -142,7 +134,7 @@ src/utils/Receipt.js
|
|||
src/Velociraptor.js
|
||||
src/VelocityBounce.js
|
||||
src/WhoIsTyping.js
|
||||
src/wrappers/WithMatrixClient.js
|
||||
src/wrappers/withMatrixClient.js
|
||||
test/all-tests.js
|
||||
test/components/structures/login/Registration-test.js
|
||||
test/components/structures/MessagePanel-test.js
|
||||
|
|
13
.eslintrc.js
13
.eslintrc.js
|
@ -40,6 +40,19 @@ module.exports = {
|
|||
}],
|
||||
"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/require-parameter-type": ["warn", {
|
||||
"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)
|
||||
===================================================================================================
|
||||
[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
|
||||
|
||||
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
|
||||
(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
|
||||
|
||||
# re-run the linter, excluding any files known to have errors or warnings.
|
||||
./node_modules/.bin/eslint --max-warnings 0 \
|
||||
--ignore-path .eslintignore.errorfiles \
|
||||
src test
|
||||
npm run lintwithexclusions
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-react-sdk-*.tgz
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "0.9.7",
|
||||
"version": "0.10.6",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -39,8 +39,9 @@
|
|||
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
||||
"lint": "eslint src/",
|
||||
"lintall": "eslint src/ test/",
|
||||
"lintwithexclusions": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
|
||||
"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-multi": "karma start"
|
||||
},
|
||||
|
@ -66,7 +67,7 @@
|
|||
"isomorphic-fetch": "^2.2.1",
|
||||
"linkifyjs": "^2.1.3",
|
||||
"lodash": "^4.13.1",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-js-sdk": "0.8.4",
|
||||
"optimist": "^0.6.1",
|
||||
"prop-types": "^15.5.8",
|
||||
"react": "^15.4.0",
|
||||
|
@ -99,7 +100,7 @@
|
|||
"eslint-config-google": "^0.7.1",
|
||||
"eslint-plugin-babel": "^4.0.1",
|
||||
"eslint-plugin-flowtype": "^2.30.0",
|
||||
"eslint-plugin-react": "^6.9.0",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"expect": "^1.16.0",
|
||||
"json-loader": "^0.5.3",
|
||||
"karma": "^1.7.0",
|
||||
|
|
|
@ -6,6 +6,4 @@ npm run test
|
|||
./.travis-test-riot.sh
|
||||
|
||||
# run the linter, but exclude any files known to have errors or warnings.
|
||||
./node_modules/.bin/eslint --max-warnings 0 \
|
||||
--ignore-path .eslintignore.errorfiles \
|
||||
src test
|
||||
npm run lintwithexclusions
|
||||
|
|
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; }
|
||||
|
||||
setupScreenSharingForIframe() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the application, without neccessarily reloading
|
||||
* any application code
|
||||
|
|
|
@ -65,7 +65,7 @@ module.exports = {
|
|||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
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) {
|
||||
// TODO: use standard date localize function provided in counterpart
|
||||
return _t('%(weekDayName)s %(time)s', {
|
||||
|
@ -78,7 +78,7 @@ module.exports = {
|
|||
weekDayName: days[date.getDay()],
|
||||
monthName: months[date.getMonth()],
|
||||
day: date.getDate(),
|
||||
time: this.formatTime(date),
|
||||
time: this.formatTime(date, showTwelveHour),
|
||||
});
|
||||
}
|
||||
return this.formatFullDate(date, showTwelveHour);
|
||||
|
@ -92,13 +92,13 @@ module.exports = {
|
|||
monthName: months[date.getMonth()],
|
||||
day: date.getDate(),
|
||||
fullYear: date.getFullYear(),
|
||||
time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date),
|
||||
time: this.formatTime(date, showTwelveHour),
|
||||
});
|
||||
},
|
||||
|
||||
formatTime: function(date, showTwelveHour=false) {
|
||||
if (showTwelveHour) {
|
||||
return twelveHourTime(date);
|
||||
return twelveHourTime(date);
|
||||
}
|
||||
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 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.
|
||||
|
@ -31,13 +32,33 @@ emojione.imagePathPNG = 'emojione/png/';
|
|||
// Use SVGs for emojis
|
||||
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 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
|
||||
* because we want to include emoji shortnames in title text
|
||||
*/
|
||||
export function unicodeToImage(str) {
|
||||
function unicodeToImage(str) {
|
||||
let replaceWith, unicode, alt, short, fname;
|
||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||
|
||||
|
@ -127,7 +148,7 @@ export function processHtmlForSending(html: string): string {
|
|||
* of that HTML.
|
||||
*/
|
||||
export function sanitizedHtmlNode(insaneHtml) {
|
||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||
}
|
||||
|
@ -136,7 +157,7 @@ const sanitizeHtmlParams = {
|
|||
allowedTags: [
|
||||
'font', // custom to matrix for IRC-style font coloring
|
||||
'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',
|
||||
'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");
|
||||
let body = isHtml ? content.formatted_body : escape(content.body);
|
||||
|
||||
let bodyHasEmoji = false;
|
||||
|
||||
var safeBody;
|
||||
// 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
|
||||
|
@ -392,17 +415,20 @@ export function bodyToHtml(content, highlights, opts) {
|
|||
};
|
||||
}
|
||||
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
|
||||
safeBody = unicodeToImage(safeBody);
|
||||
safeBody = addCodeCopyButton(safeBody);
|
||||
bodyHasEmoji = containsEmoji(body);
|
||||
if (bodyHasEmoji) safeBody = unicodeToImage(safeBody);
|
||||
}
|
||||
finally {
|
||||
delete sanitizeHtmlParams.textFilter;
|
||||
}
|
||||
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
||||
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||
let emojiBody = false;
|
||||
if (bodyHasEmoji) {
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
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({
|
||||
'mx_EventTile_body': true,
|
||||
|
@ -412,23 +438,6 @@ export function bodyToHtml(content, highlights, opts) {
|
|||
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) {
|
||||
return {
|
||||
__html: unicodeToImage(escape(text)),
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import commonmark from 'commonmark';
|
||||
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
|
||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -33,9 +34,16 @@ import Modal from './Modal';
|
|||
* }
|
||||
*/
|
||||
|
||||
const MAX_PENDING_ENCRYPTED = 20;
|
||||
|
||||
const Notifier = {
|
||||
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) {
|
||||
return TextForEvent.textForEvent(ev);
|
||||
},
|
||||
|
@ -89,17 +97,18 @@ const Notifier = {
|
|||
_playAudioNotification: function(ev, room) {
|
||||
const e = document.getElementById("messageAudio");
|
||||
if (e) {
|
||||
e.load();
|
||||
e.play();
|
||||
}
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
|
||||
this.boundOnEvent = this.onEvent.bind(this);
|
||||
this.boundOnSyncStateChange = this.onSyncStateChange.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('Event.decrypted', this.boundOnEventDecrypted);
|
||||
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
||||
this.toolbarHidden = false;
|
||||
this.isSyncing = false;
|
||||
|
@ -107,8 +116,9 @@ const Notifier = {
|
|||
|
||||
stop: function() {
|
||||
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('Event.decrypted', this.boundOnEventDecrypted);
|
||||
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
||||
}
|
||||
this.isSyncing = false;
|
||||
|
@ -237,23 +247,30 @@ const Notifier = {
|
|||
}
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||
if (toStartOfTimeline) return;
|
||||
if (!room) return;
|
||||
onEvent: function(ev) {
|
||||
if (!this.isSyncing) return; // don't alert for any messages initially
|
||||
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 (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 it's an encrypted event and the type is still 'm.room.encrypted',
|
||||
// it hasn't yet been decrypted, so wait until it is.
|
||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) {
|
||||
this.pendingEncryptedEventIds.push(ev.getId());
|
||||
// don't let the list fill up indefinitely
|
||||
while (this.pendingEncryptedEventIds.length > MAX_PENDING_ENCRYPTED) {
|
||||
this.pendingEncryptedEventIds.shift();
|
||||
}
|
||||
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) {
|
||||
|
@ -273,6 +290,20 @@ const Notifier = {
|
|||
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) {
|
||||
|
|
|
@ -28,7 +28,7 @@ export function inviteToRoom(roomId, addr) {
|
|||
|
||||
if (addrType == 'email') {
|
||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||
} else if (addrType == 'mx') {
|
||||
} else if (addrType == 'mx-user-id') {
|
||||
return MatrixClientPeg.get().invite(roomId, addr);
|
||||
} else {
|
||||
throw new Error('Unsupported address');
|
||||
|
@ -50,8 +50,8 @@ export function inviteMultipleToRoom(roomId, addrs) {
|
|||
}
|
||||
|
||||
export function showStartChatInviteDialog() {
|
||||
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
|
||||
Modal.createTrackedDialog('Start a chat', '', UserPickerDialog, {
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
||||
title: _t('Start a chat'),
|
||||
description: _t("Who would you like to communicate with?"),
|
||||
placeholder: _t("Email, name or matrix ID"),
|
||||
|
@ -61,8 +61,8 @@ export function showStartChatInviteDialog() {
|
|||
}
|
||||
|
||||
export function showRoomInviteDialog(roomId) {
|
||||
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
|
||||
Modal.createTrackedDialog('Chat Invite', '', UserPickerDialog, {
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, {
|
||||
title: _t('Invite new room members'),
|
||||
description: _t('Who would you like to add to this room?'),
|
||||
button: _t('Send Invites'),
|
||||
|
@ -127,7 +127,7 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
|
|||
}
|
||||
|
||||
function _isDmChat(addrTexts) {
|
||||
if (addrTexts.length === 1 && getAddressType(addrTexts[0])) {
|
||||
if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
|
@ -76,10 +76,13 @@ class ScalarAuthClient {
|
|||
return defer.promise;
|
||||
}
|
||||
|
||||
getScalarInterfaceUrlForRoom(roomId, screen) {
|
||||
getScalarInterfaceUrlForRoom(roomId, screen, id) {
|
||||
var url = SdkConfig.get().integrations_ui_url;
|
||||
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
||||
url += "&room_id=" + encodeURIComponent(roomId);
|
||||
if (id) {
|
||||
url += '&integ_id=' + encodeURIComponent(id);
|
||||
}
|
||||
if (screen) {
|
||||
url += '&screen=' + encodeURIComponent(screen);
|
||||
}
|
||||
|
|
|
@ -84,6 +84,9 @@ class Skinner {
|
|||
// 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
|
||||
// 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) {
|
||||
global.mxSkinner = new Skinner();
|
||||
}
|
||||
|
|
|
@ -240,6 +240,59 @@ const commands = {
|
|||
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
|
||||
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
||||
if (args) {
|
||||
|
@ -292,6 +345,13 @@ const commands = {
|
|||
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: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, 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
|
||||
limitations under the License.
|
||||
*/
|
||||
import MatrixClientPeg from "./MatrixClientPeg";
|
||||
import CallHandler from "./CallHandler";
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import CallHandler from './CallHandler';
|
||||
import { _t } from './languageHandler';
|
||||
import * as Roles from './Roles';
|
||||
|
||||
function textForMemberEvent(ev) {
|
||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||
var senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||
var targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||
var ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
var reason = ev.getContent().reason ? (
|
||||
_t('Reason') + ': ' + ev.getContent().reason
|
||||
) : "";
|
||||
switch (ev.getContent().membership) {
|
||||
case 'invite':
|
||||
var threePidContent = ev.getContent().third_party_invite;
|
||||
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||
const prevContent = ev.getPrevContent();
|
||||
const content = ev.getContent();
|
||||
|
||||
const ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
||||
switch (content.membership) {
|
||||
case 'invite': {
|
||||
const threePidContent = content.third_party_invite;
|
||||
if (threePidContent) {
|
||||
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 {
|
||||
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())) {
|
||||
return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName});
|
||||
}
|
||||
else {
|
||||
return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName});
|
||||
return _t('%(senderName)s requested a VoIP conference.', {senderName});
|
||||
} else {
|
||||
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'ban':
|
||||
return _t(
|
||||
'%(senderName)s banned %(targetName)s.',
|
||||
{senderName: senderName, targetName: targetName}
|
||||
) + ' ' + reason;
|
||||
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||
case 'join':
|
||||
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
|
||||
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().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});
|
||||
} else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
|
||||
return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname});
|
||||
} else if (ev.getPrevContent().displayname && !ev.getContent().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) {
|
||||
return _t('%(senderName)s removed their profile picture.', {senderName: senderName});
|
||||
} else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
|
||||
return _t('%(senderName)s changed their profile picture.', {senderName: senderName});
|
||||
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
|
||||
return _t('%(senderName)s set a profile picture.', {senderName: senderName});
|
||||
if (prevContent && prevContent.membership === 'join') {
|
||||
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {
|
||||
senderName,
|
||||
oldDisplayName: prevContent.displayname,
|
||||
displayName: content.displayname,
|
||||
});
|
||||
} else if (!prevContent.displayname && content.displayname) {
|
||||
return _t('%(senderName)s set their display name to %(displayName)s.', {
|
||||
senderName,
|
||||
displayName: content.displayname,
|
||||
});
|
||||
} 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 {
|
||||
// suppress null rejoins
|
||||
return '';
|
||||
|
@ -71,73 +82,69 @@ function textForMemberEvent(ev) {
|
|||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||
return _t('VoIP conference started.');
|
||||
}
|
||||
else {
|
||||
return _t('%(targetName)s joined the room.', {targetName: targetName});
|
||||
} else {
|
||||
return _t('%(targetName)s joined the room.', {targetName});
|
||||
}
|
||||
}
|
||||
case 'leave':
|
||||
if (ev.getSender() === ev.getStateKey()) {
|
||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||
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") {
|
||||
return _t('%(targetName)s rejected the invitation.', {targetName: targetName});
|
||||
}
|
||||
else {
|
||||
return _t('%(targetName)s left the room.', {targetName: targetName});
|
||||
}
|
||||
}
|
||||
else if (ev.getPrevContent().membership === "ban") {
|
||||
return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName});
|
||||
}
|
||||
else if (ev.getPrevContent().membership === "join") {
|
||||
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});
|
||||
} else if (prevContent.membership === "ban") {
|
||||
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
||||
} else if (prevContent.membership === "join") {
|
||||
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||
} else if (prevContent.membership === "invite") {
|
||||
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
||||
senderName,
|
||||
targetName,
|
||||
}) + ' ' + reason;
|
||||
} else {
|
||||
return _t('%(targetName)s left the room.', {targetName});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function textForTopicEvent(ev) {
|
||||
var 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});
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
|
||||
senderDisplayName,
|
||||
topic: ev.getContent().topic,
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
var message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} 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;
|
||||
}
|
||||
|
||||
function textForCallAnswerEvent(event) {
|
||||
var senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
||||
return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported;
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
||||
}
|
||||
|
||||
function textForCallHangupEvent(event) {
|
||||
|
@ -159,48 +166,52 @@ function textForCallHangupEvent(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?
|
||||
var type = "voice";
|
||||
let callType = "voice";
|
||||
if (event.getContent().offer && event.getContent().offer.sdp &&
|
||||
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
|
||||
type = "video";
|
||||
callType = "video";
|
||||
}
|
||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
||||
return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported;
|
||||
const supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
||||
return _t('%(senderName)s placed a %(callType)s call.', {senderName, callType}) + ' ' + supported;
|
||||
}
|
||||
|
||||
function textForThreePidInviteEvent(event) {
|
||||
var 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});
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
||||
senderName,
|
||||
targetDisplayName: event.getContent().display_name,
|
||||
});
|
||||
}
|
||||
|
||||
function textForHistoryVisibilityEvent(event) {
|
||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
||||
var vis = event.getContent().history_visibility;
|
||||
// XXX: This i18n just isn't going to work for languages with different sentence structure.
|
||||
var text = _t('%(senderName)s made future room history visible to', {senderName: senderName}) + ' ';
|
||||
if (vis === "invited") {
|
||||
text += _t('all room members, from the point they are invited') + '.';
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
switch (event.getContent().history_visibility) {
|
||||
case 'invited':
|
||||
return _t('%(senderName)s made future room history visible to all room members, '
|
||||
+ 'from the point they are invited.', {senderName});
|
||||
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) {
|
||||
var 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});
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
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
|
||||
|
@ -211,18 +222,18 @@ function textForPowerEvent(event) {
|
|||
}
|
||||
const userDefault = event.getContent().users_default || 0;
|
||||
// Construct set of userIds
|
||||
let users = [];
|
||||
const users = [];
|
||||
Object.keys(event.getContent().users).forEach(
|
||||
(userId) => {
|
||||
if (users.indexOf(userId) === -1) users.push(userId);
|
||||
}
|
||||
},
|
||||
);
|
||||
Object.keys(event.getPrevContent().users).forEach(
|
||||
(userId) => {
|
||||
if (users.indexOf(userId) === -1) users.push(userId);
|
||||
}
|
||||
},
|
||||
);
|
||||
let diff = [];
|
||||
const diff = [];
|
||||
// XXX: This is also surely broken for i18n
|
||||
users.forEach((userId) => {
|
||||
// Previous power level
|
||||
|
@ -231,11 +242,11 @@ function textForPowerEvent(event) {
|
|||
const to = event.getContent().users[userId];
|
||||
if (to !== from) {
|
||||
diff.push(
|
||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||
userId: userId,
|
||||
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.', {
|
||||
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.name': textForRoomNameEvent,
|
||||
'm.room.topic': textForTopicEvent,
|
||||
'm.room.member': textForMemberEvent,
|
||||
'm.call.invite': textForCallInviteEvent,
|
||||
'm.call.answer': textForCallAnswerEvent,
|
||||
'm.call.hangup': textForCallHangupEvent,
|
||||
'm.room.name': textForRoomNameEvent,
|
||||
'm.room.topic': textForTopicEvent,
|
||||
'm.room.member': textForMemberEvent,
|
||||
'm.call.invite': textForCallInviteEvent,
|
||||
'm.call.answer': textForCallAnswerEvent,
|
||||
'm.call.hangup': textForCallHangupEvent,
|
||||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||
'm.room.encryption': textForEncryptionEvent,
|
||||
'm.room.power_levels': textForPowerEvent,
|
||||
|
||||
'im.vector.modular.widgets': textForWidgetEvent,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
textForEvent: function(ev) {
|
||||
var hdlr = handlers[ev.getType()];
|
||||
if (!hdlr) return "";
|
||||
const hdlr = handlers[ev.getType()];
|
||||
if (!hdlr) return '';
|
||||
return hdlr(ev);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -16,11 +16,12 @@ limitations under the License.
|
|||
|
||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||
|
||||
const mxidRegex = /^@\S+:\S+$/;
|
||||
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||
const mxRoomIdRegex = /^!\S+:\S+$/;
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
export const addressTypes = [
|
||||
'mx', 'email',
|
||||
'mx-user-id', 'mx-room-id', 'email',
|
||||
];
|
||||
|
||||
// PropType definition for an object describing
|
||||
|
@ -41,13 +42,16 @@ export const UserAddressType = PropTypes.shape({
|
|||
|
||||
export function getAddressType(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
|
||||
if (isEmailAddress) {
|
||||
return 'email';
|
||||
} else if (isMatrixId) {
|
||||
return 'mx';
|
||||
} else if (isUserId) {
|
||||
return 'mx-user-id';
|
||||
} else if (isRoomId) {
|
||||
return 'mx-room-id';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -33,11 +33,17 @@ export default {
|
|||
// XXX: Always use default, ignore localStorage and remove from labs
|
||||
override: true,
|
||||
},
|
||||
{
|
||||
name: "-",
|
||||
id: 'feature_groups',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
|
||||
// horrible but it works. The locality makes this somewhat more palatable.
|
||||
doTranslations: function() {
|
||||
this.LABS_FEATURES[0].name = _t("Matrix Apps");
|
||||
this.LABS_FEATURES[1].name = _t("Groups");
|
||||
},
|
||||
|
||||
loadProfileInfo: function() {
|
||||
|
|
|
@ -18,6 +18,12 @@ var MatrixClientPeg = require("./MatrixClientPeg");
|
|||
import { _t } from './languageHandler';
|
||||
|
||||
module.exports = {
|
||||
usersTypingApartFromMeAndIgnored: function(room) {
|
||||
return this.usersTyping(
|
||||
room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers())
|
||||
);
|
||||
},
|
||||
|
||||
usersTypingApartFromMe: function(room) {
|
||||
return this.usersTyping(
|
||||
room, [MatrixClientPeg.get().credentials.userId]
|
||||
|
|
|
@ -136,13 +136,13 @@ export default React.createClass({
|
|||
) }
|
||||
</p>
|
||||
<div className='error'>
|
||||
{this.state.errStr}
|
||||
{ this.state.errStr }
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputTable'>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase1'>
|
||||
{_t("Enter passphrase")}
|
||||
{ _t("Enter passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
|
@ -155,7 +155,7 @@ export default React.createClass({
|
|||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase2'>
|
||||
{_t("Confirm passphrase")}
|
||||
{ _t("Confirm passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
|
@ -172,7 +172,7 @@ export default React.createClass({
|
|||
disabled={disableForm}
|
||||
/>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -134,13 +134,13 @@ export default React.createClass({
|
|||
) }
|
||||
</p>
|
||||
<div className='error'>
|
||||
{this.state.errStr}
|
||||
{ this.state.errStr }
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputTable'>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='importFile'>
|
||||
{_t("File to import")}
|
||||
{ _t("File to import") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
|
@ -153,14 +153,14 @@ export default React.createClass({
|
|||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase'>
|
||||
{_t("Enter passphrase")}
|
||||
{ _t("Enter passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref='passphrase' id='passphrase'
|
||||
size='64' type='password'
|
||||
onChange={this._onFormChange}
|
||||
disabled={disableForm}/>
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -170,7 +170,7 @@ export default React.createClass({
|
|||
disabled={!this.state.enableSubmit || disableForm}
|
||||
/>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import { _t, _td } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import FuzzyMatcher from './FuzzyMatcher';
|
||||
import {TextualCompletion} from './Components';
|
||||
|
@ -27,72 +27,82 @@ const COMMANDS = [
|
|||
{
|
||||
command: '/me',
|
||||
args: '<message>',
|
||||
description: 'Displays action',
|
||||
description: _td('Displays action'),
|
||||
},
|
||||
{
|
||||
command: '/ban',
|
||||
args: '<user-id> [reason]',
|
||||
description: 'Bans user with given id',
|
||||
description: _td('Bans user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/unban',
|
||||
args: '<user-id>',
|
||||
description: 'Unbans user with given id',
|
||||
description: _td('Unbans user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/op',
|
||||
args: '<user-id> [<power-level>]',
|
||||
description: 'Define the power level of a user',
|
||||
description: _td('Define the power level of a user'),
|
||||
},
|
||||
{
|
||||
command: '/deop',
|
||||
args: '<user-id>',
|
||||
description: 'Deops user with given id',
|
||||
description: _td('Deops user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/invite',
|
||||
args: '<user-id>',
|
||||
description: 'Invites user with given id to current room',
|
||||
description: _td('Invites user with given id to current room'),
|
||||
},
|
||||
{
|
||||
command: '/join',
|
||||
args: '<room-alias>',
|
||||
description: 'Joins room with given alias',
|
||||
description: _td('Joins room with given alias'),
|
||||
},
|
||||
{
|
||||
command: '/part',
|
||||
args: '[<room-alias>]',
|
||||
description: 'Leave room',
|
||||
description: _td('Leave room'),
|
||||
},
|
||||
{
|
||||
command: '/topic',
|
||||
args: '<topic>',
|
||||
description: 'Sets the room topic',
|
||||
description: _td('Sets the room topic'),
|
||||
},
|
||||
{
|
||||
command: '/kick',
|
||||
args: '<user-id> [reason]',
|
||||
description: 'Kicks user with given id',
|
||||
description: _td('Kicks user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/nick',
|
||||
args: '<display-name>',
|
||||
description: 'Changes your display nickname',
|
||||
description: _td('Changes your display nickname'),
|
||||
},
|
||||
{
|
||||
command: '/ddg',
|
||||
args: '<query>',
|
||||
description: 'Searches DuckDuckGo for results',
|
||||
description: _td('Searches DuckDuckGo for results'),
|
||||
},
|
||||
{
|
||||
command: '/tint',
|
||||
args: '<color1> [<color2>]',
|
||||
description: 'Changes colour scheme of current room',
|
||||
description: _td('Changes colour scheme of current room'),
|
||||
},
|
||||
{
|
||||
command: '/verify',
|
||||
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
|
||||
];
|
||||
|
@ -119,7 +129,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
component: (<TextualCompletion
|
||||
title={result.command}
|
||||
subtitle={result.args}
|
||||
description={ _t(result.description) }
|
||||
description={_t(result.description)}
|
||||
/>),
|
||||
range,
|
||||
};
|
||||
|
@ -140,7 +150,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_block">
|
||||
{completions}
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import {PillCompletion} from './Components';
|
|||
import type {SelectionRange, Completion} from './Autocompleter';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import UserSettingsStore from '../UserSettingsStore';
|
||||
|
||||
import EmojiData from '../stripped-emoji.json';
|
||||
|
||||
|
@ -96,6 +97,10 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
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');
|
||||
|
||||
let completions = [];
|
||||
|
|
|
@ -106,7 +106,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
{completions}
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,8 @@ const USER_REGEX = /@\S*/g;
|
|||
let instance = null;
|
||||
|
||||
export default class UserProvider extends AutocompleteProvider {
|
||||
users: Array<RoomMember> = [];
|
||||
users: Array<RoomMember> = null;
|
||||
room: Room = null;
|
||||
|
||||
constructor() {
|
||||
super(USER_REGEX, {
|
||||
|
@ -54,6 +55,9 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
return [];
|
||||
}
|
||||
|
||||
// lazy-load user list into matcher
|
||||
if (this.users === null) this._makeUsers();
|
||||
|
||||
let completions = [];
|
||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command) {
|
||||
|
@ -83,7 +87,12 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
setUserListFromRoom(room: Room) {
|
||||
const events = room.getLiveTimeline().getEvents();
|
||||
this.room = room;
|
||||
this.users = null;
|
||||
}
|
||||
|
||||
_makeUsers() {
|
||||
const events = this.room.getLiveTimeline().getEvents();
|
||||
const lastSpoken = {};
|
||||
|
||||
for(const event of events) {
|
||||
|
@ -91,7 +100,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
|
@ -103,7 +112,8 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
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
|
||||
this.users.splice(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -16,6 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
|
@ -25,6 +27,9 @@ import AccessibleButton from '../views/elements/AccessibleButton';
|
|||
import Modal from '../../Modal';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import GroupStoreCache from '../../stores/GroupStoreCache';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
|
||||
const RoomSummaryType = PropTypes.shape({
|
||||
room_id: PropTypes.string.isRequired,
|
||||
profile: PropTypes.shape({
|
||||
|
@ -37,6 +42,9 @@ const RoomSummaryType = PropTypes.shape({
|
|||
const UserSummaryType = PropTypes.shape({
|
||||
summaryInfo: PropTypes.shape({
|
||||
user_id: PropTypes.string.isRequired,
|
||||
role_id: PropTypes.string,
|
||||
avatar_url: PropTypes.string,
|
||||
displayname: PropTypes.string,
|
||||
}).isRequired,
|
||||
});
|
||||
|
||||
|
@ -50,19 +58,79 @@ const CategoryRoomList = React.createClass({
|
|||
name: PropTypes.string,
|
||||
}).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() {
|
||||
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) => {
|
||||
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) {
|
||||
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>
|
||||
{catHeader}
|
||||
{roomNodes}
|
||||
return <div className="mx_GroupView_featuredThings_container">
|
||||
{ catHeader }
|
||||
{ roomNodes }
|
||||
{ addButton }
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
@ -72,6 +140,8 @@ const FeaturedRoom = React.createClass({
|
|||
|
||||
props: {
|
||||
summaryInfo: RoomSummaryType.isRequired,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
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() {
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
|
||||
const roomName = this.props.summaryInfo.profile.name ||
|
||||
this.props.summaryInfo.profile.canonical_alias ||
|
||||
_t("Unnamed Room");
|
||||
|
||||
const oobData = {
|
||||
roomId: this.props.summaryInfo.room_id,
|
||||
avatarUrl: this.props.summaryInfo.profile.avatar_url,
|
||||
name: this.props.summaryInfo.profile.name,
|
||||
name: roomName,
|
||||
};
|
||||
|
||||
let permalink = null;
|
||||
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
|
||||
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
|
||||
}
|
||||
|
||||
let roomNameNode = null;
|
||||
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 {
|
||||
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}>
|
||||
<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>;
|
||||
},
|
||||
});
|
||||
|
@ -121,19 +232,75 @@ const RoleUserList = React.createClass({
|
|||
name: PropTypes.string,
|
||||
}).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() {
|
||||
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) => {
|
||||
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) {
|
||||
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>
|
||||
{roleHeader}
|
||||
{userNodes}
|
||||
return <div className="mx_GroupView_featuredThings_container">
|
||||
{ roleHeader }
|
||||
{ userNodes }
|
||||
{ addButton }
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
@ -143,6 +310,8 @@ const FeaturedUser = React.createClass({
|
|||
|
||||
props: {
|
||||
summaryInfo: UserSummaryType.isRequired,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
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() {
|
||||
// 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 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}>
|
||||
<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>;
|
||||
},
|
||||
});
|
||||
|
||||
const GroupContext = {
|
||||
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
|
||||
};
|
||||
|
||||
CategoryRoomList.contextTypes = GroupContext;
|
||||
FeaturedRoom.contextTypes = GroupContext;
|
||||
RoleUserList.contextTypes = GroupContext;
|
||||
FeaturedUser.contextTypes = GroupContext;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupView',
|
||||
|
||||
|
@ -176,6 +390,16 @@ export default React.createClass({
|
|||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
groupStore: React.PropTypes.instanceOf(GroupStore),
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
groupStore: this._groupStore,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
summary: null,
|
||||
|
@ -183,12 +407,21 @@ export default React.createClass({
|
|||
editing: false,
|
||||
saving: false,
|
||||
uploadingAvatar: false,
|
||||
membershipBusy: false,
|
||||
publicityBusy: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
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) {
|
||||
|
@ -197,18 +430,26 @@ export default React.createClass({
|
|||
summary: null,
|
||||
error: null,
|
||||
}, () => {
|
||||
this._loadGroupFromServer(newProps.groupId);
|
||||
this._initGroupStore(newProps.groupId);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_loadGroupFromServer: function(groupId) {
|
||||
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
|
||||
_onGroupMyMembership: function(group) {
|
||||
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({
|
||||
summary: res,
|
||||
summary: this._groupStore.getSummary(),
|
||||
error: null,
|
||||
});
|
||||
}, (err) => {
|
||||
});
|
||||
this._groupStore.on('error', (err) => {
|
||||
this.setState({
|
||||
summary: null,
|
||||
error: err,
|
||||
|
@ -216,6 +457,10 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onShowRhsClick: function(ev) {
|
||||
dis.dispatch({ action: 'show_right_panel' });
|
||||
},
|
||||
|
||||
_onEditClick: function() {
|
||||
this.setState({
|
||||
editing: true,
|
||||
|
@ -281,7 +526,7 @@ export default React.createClass({
|
|||
editing: false,
|
||||
summary: null,
|
||||
});
|
||||
this._loadGroupFromServer(this.props.groupId);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
}).catch((e) => {
|
||||
this.setState({
|
||||
saving: false,
|
||||
|
@ -295,10 +540,80 @@ export default React.createClass({
|
|||
}).done();
|
||||
},
|
||||
|
||||
_getFeaturedRoomsNode() {
|
||||
const summary = this.state.summary;
|
||||
_onAcceptInviteClick: function() {
|
||||
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 categoryRooms = {};
|
||||
|
@ -315,29 +630,32 @@ export default React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
let defaultCategoryNode = null;
|
||||
if (defaultCategoryRooms.length > 0) {
|
||||
defaultCategoryNode = <CategoryRoomList rooms={defaultCategoryRooms} />;
|
||||
}
|
||||
const defaultCategoryNode = <CategoryRoomList
|
||||
rooms={defaultCategoryRooms}
|
||||
groupId={this.props.groupId}
|
||||
editing={this.state.editing} />;
|
||||
const categoryRoomNodes = Object.keys(categoryRooms).map((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">
|
||||
<div className="mx_GroupView_featuredThings_header">
|
||||
{_t('Featured Rooms:')}
|
||||
{ _t('Featured Rooms:') }
|
||||
</div>
|
||||
{defaultCategoryNode}
|
||||
{categoryRoomNodes}
|
||||
{ defaultCategoryNode }
|
||||
{ categoryRoomNodes }
|
||||
</div>;
|
||||
},
|
||||
|
||||
_getFeaturedUsersNode() {
|
||||
_getFeaturedUsersNode: function() {
|
||||
const summary = this.state.summary;
|
||||
|
||||
if (summary.users_section.users.length == 0) return null;
|
||||
|
||||
const noRoleUsers = [];
|
||||
const roleUsers = {};
|
||||
summary.users_section.users.forEach((u) => {
|
||||
|
@ -353,24 +671,121 @@ export default React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
let noRoleNode = null;
|
||||
if (noRoleUsers.length > 0) {
|
||||
noRoleNode = <RoleUserList users={noRoleUsers} />;
|
||||
}
|
||||
const noRoleNode = <RoleUserList
|
||||
users={noRoleUsers}
|
||||
groupId={this.props.groupId}
|
||||
editing={this.state.editing} />;
|
||||
const roleUserNodes = Object.keys(roleUsers).map((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">
|
||||
<div className="mx_GroupView_featuredThings_header">
|
||||
{_t('Featured Users:')}
|
||||
{ _t('Featured Users:') }
|
||||
</div>
|
||||
{noRoleNode}
|
||||
{roleUserNodes}
|
||||
{ noRoleNode }
|
||||
{ roleUserNodes }
|
||||
</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() {
|
||||
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
@ -384,8 +799,8 @@ export default React.createClass({
|
|||
let avatarNode;
|
||||
let nameNode;
|
||||
let shortDescNode;
|
||||
let rightButtons;
|
||||
let roomBody;
|
||||
const rightButtons = [];
|
||||
const headerClasses = {
|
||||
mx_GroupView_header: true,
|
||||
};
|
||||
|
@ -404,15 +819,15 @@ export default React.createClass({
|
|||
avatarNode = (
|
||||
<div className="mx_GroupView_avatarPicker">
|
||||
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||
{avatarImage}
|
||||
{ avatarImage }
|
||||
</label>
|
||||
<div className="mx_GroupView_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||
<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" />
|
||||
</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>
|
||||
);
|
||||
|
@ -428,20 +843,26 @@ export default React.createClass({
|
|||
placeholder={_t('Description')}
|
||||
tabIndex="2"
|
||||
/>;
|
||||
rightButtons = <span>
|
||||
<AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" onClick={this._onSaveClick}>
|
||||
{_t('Save')}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick}>
|
||||
<img src="img/cancel.svg" className='mx_filterFlipColor'
|
||||
width="18" height="18" alt={_t("Cancel")}/>
|
||||
</AccessibleButton>
|
||||
</span>;
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||
onClick={this._onSaveClick} key="_saveButton"
|
||||
>
|
||||
{ _t('Save') }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
rightButtons.push(
|
||||
<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>
|
||||
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
|
||||
onChange={this._onLongDescChange}
|
||||
tabIndex="3"
|
||||
/>
|
||||
{ this._getFeaturedRoomsNode() }
|
||||
{ this._getFeaturedUsersNode() }
|
||||
</div>;
|
||||
} else {
|
||||
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
||||
|
@ -452,31 +873,44 @@ export default React.createClass({
|
|||
/>;
|
||||
if (summary.profile && summary.profile.name) {
|
||||
nameNode = <div>
|
||||
<span>{summary.profile.name}</span>
|
||||
<span>{ summary.profile.name }</span>
|
||||
<span className="mx_GroupView_header_groupid">
|
||||
({this.props.groupId})
|
||||
({ this.props.groupId })
|
||||
</span>
|
||||
</div>;
|
||||
} 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;
|
||||
if (summary.profile && summary.profile.long_description) {
|
||||
description = sanitizedHtmlNode(summary.profile.long_description);
|
||||
}
|
||||
roomBody = <div>
|
||||
<div className="mx_GroupView_groupDesc">{description}</div>
|
||||
{this._getFeaturedRoomsNode()}
|
||||
{this._getFeaturedUsersNode()}
|
||||
{ this._getMembershipSection() }
|
||||
<div className="mx_GroupView_groupDesc">{ description }</div>
|
||||
{ this._getFeaturedRoomsNode() }
|
||||
{ this._getFeaturedUsersNode() }
|
||||
</div>;
|
||||
// disabled until editing works
|
||||
rightButtons = <AccessibleButton className="mx_GroupHeader_button"
|
||||
onClick={this._onEditClick} title={_t("Edit Group")}
|
||||
>
|
||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||
</AccessibleButton>;
|
||||
if (summary.user && summary.user.is_privileged) {
|
||||
rightButtons.push(
|
||||
<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>,
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
@ -486,40 +920,40 @@ export default React.createClass({
|
|||
<div className={classnames(headerClasses)}>
|
||||
<div className="mx_GroupView_header_leftCol">
|
||||
<div className="mx_GroupView_header_avatar">
|
||||
{avatarNode}
|
||||
{ avatarNode }
|
||||
</div>
|
||||
<div className="mx_GroupView_header_info">
|
||||
<div className="mx_GroupView_header_name">
|
||||
{nameNode}
|
||||
{ nameNode }
|
||||
</div>
|
||||
<div className="mx_GroupView_header_shortDesc">
|
||||
{shortDescNode}
|
||||
{ shortDescNode }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_GroupView_header_rightCol">
|
||||
{rightButtons}
|
||||
{ rightButtons }
|
||||
</div>
|
||||
</div>
|
||||
{roomBody}
|
||||
{ roomBody }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.error) {
|
||||
if (this.state.error.httpStatus === 404) {
|
||||
return (
|
||||
<div className="mx_GroupView_error">
|
||||
Group {this.props.groupId} not found
|
||||
Group { this.props.groupId } not found
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let extraText;
|
||||
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 (
|
||||
<div className="mx_GroupView_error">
|
||||
Failed to load {this.props.groupId}
|
||||
{extraText}
|
||||
Failed to load { this.props.groupId }
|
||||
{ extraText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -81,10 +81,6 @@ export default React.createClass({
|
|||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
||||
// _scrollStateMap is a map from room id to the scroll state returned by
|
||||
// RoomView.getScrollState()
|
||||
this._scrollStateMap = {};
|
||||
|
||||
CallMediaHandler.loadDevices();
|
||||
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
|
@ -116,10 +112,6 @@ export default React.createClass({
|
|||
return Boolean(MatrixClientPeg.get());
|
||||
},
|
||||
|
||||
getScrollStateForRoom: function(roomId) {
|
||||
return this._scrollStateMap[roomId];
|
||||
},
|
||||
|
||||
canResetTimelineInRoom: function(roomId) {
|
||||
if (!this.refs.roomView) {
|
||||
return true;
|
||||
|
@ -139,6 +131,9 @@ export default React.createClass({
|
|||
useCompactLayout: event.getContent().useCompactLayout,
|
||||
});
|
||||
}
|
||||
if (event.getType() === "m.ignored_user_list") {
|
||||
dis.dispatch({action: "ignore_state_changed"});
|
||||
}
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
|
@ -246,11 +241,10 @@ export default React.createClass({
|
|||
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
opacity={this.props.middleOpacity}
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
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;
|
||||
|
||||
case PageTypes.UserSettings:
|
||||
|
@ -261,7 +255,7 @@ export default React.createClass({
|
|||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
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;
|
||||
|
||||
case PageTypes.MyGroups:
|
||||
|
@ -271,9 +265,9 @@ export default React.createClass({
|
|||
case PageTypes.CreateRoom:
|
||||
page_element = <CreateRoom
|
||||
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;
|
||||
|
||||
case PageTypes.RoomDirectory:
|
||||
|
@ -306,8 +300,9 @@ export default React.createClass({
|
|||
case PageTypes.GroupView:
|
||||
page_element = <GroupView
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -339,7 +334,7 @@ export default React.createClass({
|
|||
<div className={bodyClasses}>
|
||||
<LeftPanel
|
||||
selectedRoom={this.props.currentRoomId}
|
||||
collapsed={this.props.collapse_lhs || false}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
opacity={this.props.leftOpacity}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
|
|
|
@ -32,13 +32,12 @@ import dis from "../../dispatcher";
|
|||
import Modal from "../../Modal";
|
||||
import Tinter from "../../Tinter";
|
||||
import sdk from '../../index';
|
||||
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite';
|
||||
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import linkifyMatrix from "../../linkify-matrix";
|
||||
import * as Lifecycle from '../../Lifecycle';
|
||||
// LifecycleStore is not used but does listen to and dispatch actions
|
||||
require('../../stores/LifecycleStore');
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import PageTypes from '../../PageTypes';
|
||||
|
||||
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
|
||||
viewUserId: null,
|
||||
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
collapseLhs: false,
|
||||
collapseRhs: false,
|
||||
leftOpacity: 1.0,
|
||||
middleOpacity: 1.0,
|
||||
rightOpacity: 1.0,
|
||||
|
@ -214,9 +213,6 @@ module.exports = React.createClass({
|
|||
componentWillMount: function() {
|
||||
SdkConfig.put(this.props.config);
|
||||
|
||||
this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated);
|
||||
this._onRoomViewStoreUpdated();
|
||||
|
||||
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
|
||||
|
||||
// Used by _viewRoom before getting state from sync
|
||||
|
@ -353,7 +349,6 @@ module.exports = React.createClass({
|
|||
UDEHandler.stopListening();
|
||||
window.removeEventListener("focus", this.onFocus);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this._roomViewStoreToken.remove();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
|
@ -439,7 +434,7 @@ module.exports = React.createClass({
|
|||
break;
|
||||
case 'view_user':
|
||||
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
|
||||
if (this.state.collapse_rhs) {
|
||||
if (this.state.collapseRhs) {
|
||||
setTimeout(()=>{
|
||||
dis.dispatch({
|
||||
action: 'show_right_panel',
|
||||
|
@ -521,22 +516,22 @@ module.exports = React.createClass({
|
|||
break;
|
||||
case 'hide_left_panel':
|
||||
this.setState({
|
||||
collapse_lhs: true,
|
||||
collapseLhs: true,
|
||||
});
|
||||
break;
|
||||
case 'show_left_panel':
|
||||
this.setState({
|
||||
collapse_lhs: false,
|
||||
collapseLhs: false,
|
||||
});
|
||||
break;
|
||||
case 'hide_right_panel':
|
||||
this.setState({
|
||||
collapse_rhs: true,
|
||||
collapseRhs: true,
|
||||
});
|
||||
break;
|
||||
case 'show_right_panel':
|
||||
this.setState({
|
||||
collapse_rhs: false,
|
||||
collapseRhs: false,
|
||||
});
|
||||
break;
|
||||
case 'ui_opacity': {
|
||||
|
@ -587,10 +582,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdated: function() {
|
||||
this.setState({ currentRoomId: RoomViewStore.getRoomId() });
|
||||
},
|
||||
|
||||
_setPage: function(pageType) {
|
||||
this.setState({
|
||||
page_type: pageType,
|
||||
|
@ -677,10 +668,10 @@ module.exports = React.createClass({
|
|||
this.focusComposer = true;
|
||||
|
||||
const newState = {
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
page_type: PageTypes.RoomView,
|
||||
thirdPartyInvite: roomInfo.third_party_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
autoJoin: roomInfo.auto_join,
|
||||
};
|
||||
|
||||
if (roomInfo.room_alias) {
|
||||
|
@ -860,7 +851,7 @@ module.exports = React.createClass({
|
|||
title: _t("Leave room"),
|
||||
description: (
|
||||
<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>
|
||||
),
|
||||
onFinished: (shouldLeave) => {
|
||||
|
@ -1000,8 +991,8 @@ module.exports = React.createClass({
|
|||
this.setStateForNewView({
|
||||
view: VIEWS.LOGIN,
|
||||
ready: false,
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
collapseLhs: false,
|
||||
collapseRhs: false,
|
||||
currentRoomId: null,
|
||||
page_type: PageTypes.RoomDirectory,
|
||||
});
|
||||
|
@ -1066,10 +1057,13 @@ module.exports = React.createClass({
|
|||
self.setState({ready: true});
|
||||
});
|
||||
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({
|
||||
action: 'incoming_call',
|
||||
call: call,
|
||||
});
|
||||
}, true);
|
||||
});
|
||||
cli.on('Session.logged_out', function(call) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -1454,7 +1448,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
|
||||
{ _t('Logout') }
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -65,7 +65,7 @@ module.exports = React.createClass({
|
|||
suppressFirstDateSeparator: React.PropTypes.bool,
|
||||
|
||||
// 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
|
||||
// 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
|
||||
_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');
|
||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||
return false; // no tile = no show
|
||||
|
@ -339,6 +343,15 @@ module.exports = React.createClass({
|
|||
for (;i + 1 < this.props.events.length; i++) {
|
||||
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) ||
|
||||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
||||
break;
|
||||
|
@ -349,16 +362,16 @@ module.exports = React.createClass({
|
|||
readMarkerInMels = true;
|
||||
}
|
||||
|
||||
// Ignore redacted/hidden member events
|
||||
if (!this._shouldShowEvent(collapsedMxEv)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
summarisedEvents.push(collapsedMxEv);
|
||||
}
|
||||
|
||||
let highlightInMels = false;
|
||||
|
||||
// At this point, i = the index of the last event in the summary sequence
|
||||
let eventTiles = summarisedEvents.map((e) => {
|
||||
if (e.getId() === this.props.highlightedEventId) {
|
||||
highlightInMels = true;
|
||||
}
|
||||
// In order to prevent DateSeparators from appearing in the expanded form
|
||||
// of MemberEventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
|
@ -372,15 +385,13 @@ module.exports = React.createClass({
|
|||
eventTiles = null;
|
||||
}
|
||||
|
||||
ret.push(
|
||||
<MemberEventListSummary
|
||||
key={key}
|
||||
events={summarisedEvents}
|
||||
onToggle={this._onWidgetLoad} // Update scroll state
|
||||
>
|
||||
{eventTiles}
|
||||
</MemberEventListSummary>
|
||||
);
|
||||
ret.push(<MemberEventListSummary key={key}
|
||||
events={summarisedEvents}
|
||||
onToggle={this._onWidgetLoad} // Update scroll state
|
||||
startExpanded={highlightInMels}
|
||||
>
|
||||
{eventTiles}
|
||||
</MemberEventListSummary>);
|
||||
|
||||
if (readMarkerInMels) {
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
|
@ -487,7 +498,7 @@ module.exports = React.createClass({
|
|||
var scrollToken = mxEv.status ? undefined : eventId;
|
||||
|
||||
var readReceipts;
|
||||
if (this.props.manageReadReceipts) {
|
||||
if (this.props.showReadReceipts) {
|
||||
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
||||
}
|
||||
ret.push(
|
||||
|
@ -545,6 +556,9 @@ module.exports = React.createClass({
|
|||
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
|
||||
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);
|
||||
if (!member) {
|
||||
return; // ignore unknown user IDs
|
||||
|
|
|
@ -39,7 +39,7 @@ const GroupTile = React.createClass({
|
|||
},
|
||||
|
||||
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>
|
||||
<div>{_t('You are a member of these groups:')}</div>
|
||||
{groupNodes}
|
||||
<div>{ _t('You are a member of these groups:') }</div>
|
||||
{ groupNodes }
|
||||
</div>;
|
||||
} else if (this.state.error) {
|
||||
content = <div className="mx_MyGroups_error">
|
||||
{_t('Error whilst fetching joined groups')}
|
||||
{ _t('Error whilst fetching joined groups') }
|
||||
</div>;
|
||||
} else {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
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_createBox">
|
||||
<div className="mx_MyGroups_joinCreateHeader">
|
||||
{_t('Create a new group')}
|
||||
{ _t('Create a new group') }
|
||||
</div>
|
||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
{_t(
|
||||
{ _t(
|
||||
'Create a group to represent your community! '+
|
||||
'Define a set of rooms and your own custom homepage '+
|
||||
'to mark out your space in the Matrix universe.',
|
||||
)}
|
||||
) }
|
||||
</div>
|
||||
<div className="mx_MyGroups_joinBox">
|
||||
<div className="mx_MyGroups_joinCreateHeader">
|
||||
{_t('Join an existing group')}
|
||||
{ _t('Join an existing group') }
|
||||
</div>
|
||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
{_tJsx(
|
||||
'To join an exisitng group you\'ll have to '+
|
||||
{ _tJsx(
|
||||
'To join an existing group you\'ll have to '+
|
||||
'know its group identifier; this will look '+
|
||||
'something like <i>+example:matrix.org</i>.',
|
||||
/<i>(.*)<\/i>/,
|
||||
(sub) => <i>{sub}</i>,
|
||||
)}
|
||||
(sub) => <i>{ sub }</i>,
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_MyGroups_content">
|
||||
{content}
|
||||
{ content }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
|
|
@ -121,7 +121,7 @@ module.exports = React.createClass({
|
|||
|
||||
onRoomMemberTyping: function(ev, member) {
|
||||
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
|
||||
// - File uploading - uploadFile()
|
||||
|
||||
import shouldHideEvent from "../../shouldHideEvent";
|
||||
|
||||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
import Promise from 'bluebird';
|
||||
|
@ -45,6 +47,7 @@ import KeyCode from '../../KeyCode';
|
|||
import UserProvider from '../../autocomplete/UserProvider';
|
||||
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
|
||||
let DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -120,6 +123,9 @@ module.exports = React.createClass({
|
|||
// store the error here.
|
||||
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
|
||||
// the end of the live timeline. It has the effect of hiding the
|
||||
// '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("accountData", this.onAccountData);
|
||||
|
||||
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
|
@ -152,6 +160,22 @@ module.exports = React.createClass({
|
|||
if (this.unmounted) {
|
||||
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 = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
|
@ -159,16 +183,11 @@ module.exports = React.createClass({
|
|||
roomLoadError: RoomViewStore.getRoomLoadError(),
|
||||
joining: RoomViewStore.isJoining(),
|
||||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
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
|
||||
console.log(
|
||||
'RVS update:',
|
||||
|
@ -177,7 +196,6 @@ module.exports = React.createClass({
|
|||
'loading?', newState.roomLoading,
|
||||
'joining?', newState.joining,
|
||||
'initial?', initial,
|
||||
'waiting?', newState.waitingForRoom,
|
||||
'shouldPeek?', newState.shouldPeek,
|
||||
);
|
||||
|
||||
|
@ -185,6 +203,25 @@ module.exports = React.createClass({
|
|||
// the RoomView instance
|
||||
if (initial) {
|
||||
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
|
||||
|
@ -193,22 +230,20 @@ module.exports = React.createClass({
|
|||
newState.searchResults = null;
|
||||
}
|
||||
|
||||
// Store the scroll state for the previous room so that we can return to this
|
||||
// position when viewing this room in future.
|
||||
if (this.state.roomId !== newState.roomId) {
|
||||
this._updateScrollMap(this.state.roomId);
|
||||
}
|
||||
this.setState(newState);
|
||||
// At this point, newState.roomId could be null (e.g. the alias might not
|
||||
// have been resolved yet) so anything called here must handle this case.
|
||||
|
||||
this.setState(newState, () => {
|
||||
// At this point, this.state.roomId could be null (e.g. the alias might not
|
||||
// have been resolved yet) so anything called here must handle this case.
|
||||
if (initial) {
|
||||
this._onHaveRoom();
|
||||
}
|
||||
});
|
||||
// We pass the new state into this function for it to read: it needs to
|
||||
// observe the new state but we don't want to put it in the setState
|
||||
// callback because this would prevent the setStates from being batched,
|
||||
// ie. cause it to render RoomView twice rather than the once that is necessary.
|
||||
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:
|
||||
// - 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)
|
||||
|
@ -224,23 +259,15 @@ module.exports = React.createClass({
|
|||
// 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,
|
||||
// making it impossible to indicate a newly joined room.
|
||||
const room = this.state.room;
|
||||
if (room) {
|
||||
this.setState({
|
||||
unsentMessageError: this._getUnsentMessageError(room),
|
||||
showApps: this._shouldShowApps(room),
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}
|
||||
if (!this.state.joining && this.state.roomId) {
|
||||
if (!joining && roomId) {
|
||||
if (this.props.autoJoin) {
|
||||
this.onJoinButtonClicked();
|
||||
} else if (!room && this.state.shouldPeek) {
|
||||
console.log("Attempting to peek into room %s", this.state.roomId);
|
||||
} else if (!room && shouldPeek) {
|
||||
console.log("Attempting to peek into room %s", roomId);
|
||||
this.setState({
|
||||
peekLoading: true,
|
||||
});
|
||||
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
|
||||
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
||||
this.setState({
|
||||
room: room,
|
||||
peekLoading: false,
|
||||
|
@ -336,7 +363,9 @@ module.exports = React.createClass({
|
|||
this.unmounted = true;
|
||||
|
||||
// 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) {
|
||||
// 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
|
||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||
// no change
|
||||
}
|
||||
else {
|
||||
} else if (!shouldHideEvent(ev, this._syncedSettings)) {
|
||||
this.setState((state, props) => {
|
||||
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) {
|
||||
if (!room || room.roomId !== this.state.roomId) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
room: room,
|
||||
waitingForRoom: false,
|
||||
}, () => {
|
||||
this._onRoomLoaded(room);
|
||||
});
|
||||
|
@ -688,14 +703,7 @@ module.exports = React.createClass({
|
|||
|
||||
onRoomMemberMembership: function(ev, member, oldMembership) {
|
||||
if (member.userId == MatrixClientPeg.get().credentials.userId) {
|
||||
|
||||
if (member.membership === 'join') {
|
||||
this.setState({
|
||||
waitingForRoom: false,
|
||||
});
|
||||
} else {
|
||||
this.forceUpdate();
|
||||
}
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1445,10 +1453,6 @@ module.exports = React.createClass({
|
|||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
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.roomLoading || this.state.peekLoading) {
|
||||
return (
|
||||
|
@ -1482,7 +1486,7 @@ module.exports = React.createClass({
|
|||
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
|
||||
canPreview={ false } error={ this.state.roomLoadError }
|
||||
roomAlias={roomAlias}
|
||||
spinner={previewBarSpinner}
|
||||
spinner={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
room={this.state.room}
|
||||
|
@ -1525,7 +1529,7 @@ module.exports = React.createClass({
|
|||
onRejectClick={ this.onRejectButtonClicked }
|
||||
inviterName={ inviterName }
|
||||
canPreview={ false }
|
||||
spinner={previewBarSpinner}
|
||||
spinner={this.state.joining}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1600,7 +1604,7 @@ module.exports = React.createClass({
|
|||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={ this.onForgetClick }
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
spinner={previewBarSpinner}
|
||||
spinner={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
canPreview={this.state.canPeek}
|
||||
|
@ -1716,7 +1720,8 @@ module.exports = React.createClass({
|
|||
var messagePanel = (
|
||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
||||
showReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
||||
manageReadReceipts={true}
|
||||
manageReadMarkers={true}
|
||||
hidden={hideMessagePanel}
|
||||
highlightedEventId={highlightedEventId}
|
||||
|
|
|
@ -157,7 +157,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.checkFillState();
|
||||
this.checkScroll();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
|
|
|
@ -59,6 +59,7 @@ var TimelinePanel = React.createClass({
|
|||
// that room.
|
||||
timelineSet: React.PropTypes.object.isRequired,
|
||||
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||
manageReadReceipts: 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.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
|
||||
MatrixClientPeg.get().on("sync", this.onSync);
|
||||
|
||||
this._initTimeline(this.props);
|
||||
|
@ -266,6 +268,7 @@ var TimelinePanel = React.createClass({
|
|||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
client.removeListener("Room.accountData", this.onAccountData);
|
||||
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
client.removeListener("sync", this.onSync);
|
||||
}
|
||||
},
|
||||
|
@ -341,9 +344,16 @@ var TimelinePanel = React.createClass({
|
|||
newState[canPaginateOtherWayKey] = true;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
return r;
|
||||
// Don't resolve until the setState has completed: we need to let
|
||||
// the component update before we consider the pagination completed,
|
||||
// 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.updateReadMarker();
|
||||
break;
|
||||
case 'ignore_state_changed':
|
||||
this.forceUpdate();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -503,6 +516,18 @@ var TimelinePanel = React.createClass({
|
|||
}, 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) {
|
||||
this.setState({clientSyncState: state});
|
||||
},
|
||||
|
@ -1126,8 +1151,8 @@ var TimelinePanel = React.createClass({
|
|||
readMarkerEventId={ this.state.readMarkerEventId }
|
||||
readMarkerVisible={ this.state.readMarkerVisible }
|
||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||
showUrlPreview = { this.props.showUrlPreview }
|
||||
manageReadReceipts = { this.props.manageReadReceipts }
|
||||
showUrlPreview={ this.props.showUrlPreview }
|
||||
showReadReceipts={ this.props.showReadReceipts }
|
||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||
stickyBottom={ stickyBottom }
|
||||
onScroll={ this.onMessageListScroll }
|
||||
|
|
|
@ -32,7 +32,7 @@ const AddThreepid = require('../../AddThreepid');
|
|||
const SdkConfig = require('../../SdkConfig');
|
||||
import Analytics from '../../Analytics';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import * as languageHandler from '../../languageHandler';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
|
||||
|
@ -52,7 +52,7 @@ const gHVersionLabel = function(repo, token='') {
|
|||
} else {
|
||||
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).
|
||||
|
@ -63,51 +63,55 @@ const gHVersionLabel = function(repo, token='') {
|
|||
const SETTINGS_LABELS = [
|
||||
{
|
||||
id: 'autoplayGifsAndVideos',
|
||||
label: 'Autoplay GIFs and videos',
|
||||
label: _td('Autoplay GIFs and videos'),
|
||||
},
|
||||
{
|
||||
id: 'hideReadReceipts',
|
||||
label: 'Hide read receipts',
|
||||
label: _td('Hide read receipts'),
|
||||
},
|
||||
{
|
||||
id: 'dontSendTypingNotifications',
|
||||
label: "Don't send typing notifications",
|
||||
label: _td("Don't send typing notifications"),
|
||||
},
|
||||
{
|
||||
id: 'alwaysShowTimestamps',
|
||||
label: 'Always show message timestamps',
|
||||
label: _td('Always show message timestamps'),
|
||||
},
|
||||
{
|
||||
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',
|
||||
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
|
||||
label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
|
||||
},
|
||||
{
|
||||
id: 'hideAvatarDisplaynameChanges',
|
||||
label: 'Hide avatar and display name changes',
|
||||
label: _td('Hide avatar and display name changes'),
|
||||
},
|
||||
{
|
||||
id: 'useCompactLayout',
|
||||
label: 'Use compact timeline layout',
|
||||
label: _td('Use compact timeline layout'),
|
||||
},
|
||||
{
|
||||
id: 'hideRedactions',
|
||||
label: 'Hide removed messages',
|
||||
label: _td('Hide removed messages'),
|
||||
},
|
||||
{
|
||||
id: 'enableSyntaxHighlightLanguageDetection',
|
||||
label: 'Enable automatic language detection for syntax highlighting',
|
||||
label: _td('Enable automatic language detection for syntax highlighting'),
|
||||
},
|
||||
{
|
||||
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',
|
||||
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 = [
|
||||
{
|
||||
id: 'analyticsOptOut',
|
||||
label: 'Opt out of analytics',
|
||||
label: _td('Opt out of analytics'),
|
||||
fn: function(checked) {
|
||||
Analytics[checked ? 'disable' : 'enable']();
|
||||
},
|
||||
|
@ -130,7 +134,7 @@ const ANALYTICS_SETTINGS_LABELS = [
|
|||
const WEBRTC_SETTINGS_LABELS = [
|
||||
{
|
||||
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 = [
|
||||
{
|
||||
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) {
|
||||
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
|
||||
},
|
||||
|
@ -162,16 +166,44 @@ const CRYPTO_SETTINGS_LABELS = [
|
|||
const THEMES = [
|
||||
{
|
||||
id: 'theme',
|
||||
label: 'Light theme',
|
||||
label: _td('Light theme'),
|
||||
value: 'light',
|
||||
},
|
||||
{
|
||||
id: 'theme',
|
||||
label: 'Dark theme',
|
||||
label: _td('Dark theme'),
|
||||
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({
|
||||
displayName: 'UserSettings',
|
||||
|
||||
|
@ -207,6 +239,7 @@ module.exports = React.createClass({
|
|||
vectorVersion: undefined,
|
||||
rejectingInvites: false,
|
||||
mediaDevices: null,
|
||||
ignoredUsers: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -228,6 +261,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
this._refreshMediaDevices();
|
||||
this._refreshIgnoredUsers();
|
||||
|
||||
// Bulk rejecting invites:
|
||||
// /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) {
|
||||
if (payload.action === "notifier_enabled") {
|
||||
this.forceUpdate();
|
||||
} else if (payload.action === "ignore_state_changed") {
|
||||
this._refreshIgnoredUsers();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -627,7 +674,7 @@ module.exports = React.createClass({
|
|||
<div>
|
||||
<h3>Referral</h3>
|
||||
<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>
|
||||
);
|
||||
|
@ -646,7 +693,7 @@ module.exports = React.createClass({
|
|||
_renderLanguageSetting: function() {
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <div>
|
||||
<label htmlFor="languageSelector">{_t('Interface Language')}</label>
|
||||
<label htmlFor="languageSelector">{ _t('Interface Language') }</label>
|
||||
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
|
||||
className="mx_UserSettings_language"
|
||||
value={this.state.language}
|
||||
|
@ -669,7 +716,7 @@ module.exports = React.createClass({
|
|||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{_t('Autocomplete Delay (ms):')}</strong></td>
|
||||
<td><strong>{ _t('Autocomplete Delay (ms):') }</strong></td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
|
@ -690,8 +737,8 @@ module.exports = React.createClass({
|
|||
return <div className="mx_UserSettings_toggle">
|
||||
<input id="urlPreviewsDisabled"
|
||||
type="checkbox"
|
||||
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
||||
onChange={ this._onPreviewsDisabledChanged }
|
||||
defaultChecked={UserSettingsStore.getUrlPreviewsDisabled()}
|
||||
onChange={this._onPreviewsDisabledChanged}
|
||||
/>
|
||||
<label htmlFor="urlPreviewsDisabled">
|
||||
{ _t("Disable inline URL previews by default") }
|
||||
|
@ -712,13 +759,13 @@ module.exports = React.createClass({
|
|||
if (setting.fn) setting.fn(e.target.checked);
|
||||
};
|
||||
|
||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
||||
<input id={ setting.id }
|
||||
return <div className="mx_UserSettings_toggle" key={setting.id}>
|
||||
<input id={setting.id}
|
||||
type="checkbox"
|
||||
defaultChecked={ this._syncedSettings[setting.id] }
|
||||
onChange={ onChange }
|
||||
defaultChecked={this._syncedSettings[setting.id]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={ setting.id }>
|
||||
<label htmlFor={setting.id}>
|
||||
{ _t(setting.label) }
|
||||
</label>
|
||||
</div>;
|
||||
|
@ -729,6 +776,7 @@ module.exports = React.createClass({
|
|||
// to rebind the onChange each time we render
|
||||
const onChange = (e) => {
|
||||
if (e.target.checked) {
|
||||
this._syncedSettings[setting.id] = setting.value;
|
||||
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
||||
}
|
||||
dis.dispatch({
|
||||
|
@ -736,16 +784,16 @@ module.exports = React.createClass({
|
|||
value: setting.value,
|
||||
});
|
||||
};
|
||||
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
|
||||
<input id={ setting.id + "_" + setting.value }
|
||||
return <div className="mx_UserSettings_toggle" key={setting.id + "_" + setting.value}>
|
||||
<input id={setting.id + "_" + setting.value}
|
||||
type="radio"
|
||||
name={ setting.id }
|
||||
value={ setting.value }
|
||||
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
|
||||
onChange={ onChange }
|
||||
name={setting.id}
|
||||
value={setting.value}
|
||||
checked={this._syncedSettings[setting.id] === setting.value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={ setting.id + "_" + setting.value }>
|
||||
{ setting.label }
|
||||
<label htmlFor={setting.id + "_" + setting.value}>
|
||||
{ _t(setting.label) }
|
||||
</label>
|
||||
</div>;
|
||||
},
|
||||
|
@ -781,10 +829,10 @@ module.exports = React.createClass({
|
|||
<h3>{ _t("Cryptography") }</h3>
|
||||
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{_t("Device ID:")}</label>
|
||||
<span><code>{deviceId}</code></span></li>
|
||||
<li><label>{_t("Device key:")}</label>
|
||||
<span><code><b>{identityKey}</b></code></span></li>
|
||||
<li><label>{ _t("Device ID:") }</label>
|
||||
<span><code>{ deviceId }</code></span></li>
|
||||
<li><label>{ _t("Device key:") }</label>
|
||||
<span><code><b>{ identityKey }</b></code></span></li>
|
||||
</ul>
|
||||
{ importExportButtons }
|
||||
</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) {
|
||||
// TODO: this ought to be a separate component so that we don't need
|
||||
// to rebind the onChange each time we render
|
||||
|
@ -803,13 +871,13 @@ module.exports = React.createClass({
|
|||
if (setting.fn) setting.fn(e.target.checked);
|
||||
};
|
||||
|
||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
||||
<input id={ setting.id }
|
||||
return <div className="mx_UserSettings_toggle" key={setting.id}>
|
||||
<input id={setting.id}
|
||||
type="checkbox"
|
||||
defaultChecked={ this._localSettings[setting.id] }
|
||||
onChange={ onChange }
|
||||
defaultChecked={this._localSettings[setting.id]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={ setting.id }>
|
||||
<label htmlFor={setting.id}>
|
||||
{ _t(setting.label) }
|
||||
</label>
|
||||
</div>;
|
||||
|
@ -819,8 +887,8 @@ module.exports = React.createClass({
|
|||
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||
return (
|
||||
<div>
|
||||
<h3>{_t("Devices")}</h3>
|
||||
<DevicesPanel className="mx_UserSettings_section"/>
|
||||
<h3>{ _t("Devices") }</h3>
|
||||
<DevicesPanel className="mx_UserSettings_section" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -835,7 +903,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_UserSettings_section">
|
||||
<p>{ _t("Found a bug?") }</p>
|
||||
<button className="mx_UserSettings_button danger"
|
||||
onClick={this._onBugReportClicked}>{_t('Report it')}
|
||||
onClick={this._onBugReportClicked}>{ _t('Report it') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -843,13 +911,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderAnalyticsControl: function() {
|
||||
if (!SdkConfig.get().piwik) return <div/>;
|
||||
if (!SdkConfig.get().piwik) return <div />;
|
||||
|
||||
return <div>
|
||||
<h3>{ _t('Analytics') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{_t('Riot collects anonymous analytics to allow us to improve the application.')}
|
||||
{ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting )}
|
||||
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
|
||||
{ ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting ) }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
@ -879,10 +947,10 @@ module.exports = React.createClass({
|
|||
type="checkbox"
|
||||
id={feature.id}
|
||||
name={feature.id}
|
||||
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
|
||||
onChange={ onChange }
|
||||
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={feature.id}>{feature.name}</label>
|
||||
<label htmlFor={feature.id}>{ feature.name }</label>
|
||||
</div>);
|
||||
});
|
||||
|
||||
|
@ -896,7 +964,7 @@ module.exports = React.createClass({
|
|||
<h3>{ _t("Labs") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
|
||||
{features}
|
||||
{ features }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -929,10 +997,10 @@ module.exports = React.createClass({
|
|||
const platform = PlatformPeg.get();
|
||||
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
|
||||
return <div>
|
||||
<h3>{_t('Updates')}</h3>
|
||||
<h3>{ _t('Updates') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
|
||||
{_t('Check for update')}
|
||||
{ _t('Check for update') }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -958,7 +1026,7 @@ module.exports = React.createClass({
|
|||
reject = (
|
||||
<AccessibleButton className="mx_UserSettings_button danger"
|
||||
onClick={onClick}>
|
||||
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
|
||||
{ _t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length}) }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -966,7 +1034,7 @@ module.exports = React.createClass({
|
|||
return <div>
|
||||
<h3>{ _t("Bulk Options") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{reject}
|
||||
{ reject }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
@ -984,7 +1052,7 @@ module.exports = React.createClass({
|
|||
defaultChecked={settings['auto-launch']}
|
||||
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>;
|
||||
|
@ -996,7 +1064,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_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) {
|
||||
|
@ -1032,15 +1100,15 @@ module.exports = React.createClass({
|
|||
if (this.state.mediaDevices === false) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
} else if (!this.state.mediaDevices) return;
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
let microphoneDropdown = <p>{_t('No Microphones detected')}</p>;
|
||||
let webcamDropdown = <p>{_t('No Webcams detected')}</p>;
|
||||
let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
||||
let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
||||
|
||||
const defaultOption = {
|
||||
deviceId: '',
|
||||
|
@ -1057,12 +1125,12 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
microphoneDropdown = <div>
|
||||
<h4>{_t('Microphone')}</h4>
|
||||
<h4>{ _t('Microphone') }</h4>
|
||||
<Dropdown
|
||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||
value={this.state.activeAudioInput || defaultInput}
|
||||
onOptionChange={this._setAudioInput}>
|
||||
{this._mapWebRtcDevicesToSpans(audioInputs)}
|
||||
{ this._mapWebRtcDevicesToSpans(audioInputs) }
|
||||
</Dropdown>
|
||||
</div>;
|
||||
}
|
||||
|
@ -1077,25 +1145,25 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
webcamDropdown = <div>
|
||||
<h4>{_t('Camera')}</h4>
|
||||
<h4>{ _t('Camera') }</h4>
|
||||
<Dropdown
|
||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||
value={this.state.activeVideoInput || defaultInput}
|
||||
onOptionChange={this._setVideoInput}>
|
||||
{this._mapWebRtcDevicesToSpans(videoInputs)}
|
||||
{ this._mapWebRtcDevicesToSpans(videoInputs) }
|
||||
</Dropdown>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
{microphoneDropdown}
|
||||
{webcamDropdown}
|
||||
{ microphoneDropdown }
|
||||
{ webcamDropdown }
|
||||
</div>;
|
||||
},
|
||||
|
||||
_renderWebRtcSettings: function() {
|
||||
return <div>
|
||||
<h3>{_t('VoIP')}</h3>
|
||||
<h3>{ _t('VoIP') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) }
|
||||
{ this._renderWebRtcDeviceSettings() }
|
||||
|
@ -1161,7 +1229,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
||||
<label htmlFor={id}>{ this.nameForMedium(val.medium) }</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<input type="text" key={val.address} id={id}
|
||||
|
@ -1169,7 +1237,7 @@ module.exports = React.createClass({
|
|||
/>
|
||||
</div>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1182,16 +1250,16 @@ module.exports = React.createClass({
|
|||
addEmailSection = (
|
||||
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label>{_t('Email')}</label>
|
||||
<label>{ _t('Email') }</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<EditableText
|
||||
ref="add_email_input"
|
||||
className="mx_UserSettings_editable"
|
||||
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
||||
placeholder={ _t("Add email address") }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this._onAddEmailEditFinished } />
|
||||
placeholder={_t("Add email address")}
|
||||
blurToCancel={false}
|
||||
onValueChanged={this._onAddEmailEditFinished} />
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
||||
|
@ -1239,8 +1307,8 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_UserSettings">
|
||||
<SimpleRoomHeader
|
||||
title={ _t("Settings") }
|
||||
onCancelClick={ this.props.onClose }
|
||||
title={_t("Settings")}
|
||||
onCancelClick={this.props.onClose}
|
||||
/>
|
||||
|
||||
<GeminiScrollbar className="mx_UserSettings_body"
|
||||
|
@ -1258,21 +1326,21 @@ module.exports = React.createClass({
|
|||
<ChangeDisplayName />
|
||||
</div>
|
||||
</div>
|
||||
{threepidsSection}
|
||||
{ threepidsSection }
|
||||
</div>
|
||||
|
||||
<div className="mx_UserSettings_avatarPicker">
|
||||
<div onClick={ this.onAvatarPickerClick }>
|
||||
<div onClick={this.onAvatarPickerClick}>
|
||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img" />
|
||||
</div>
|
||||
<div className="mx_UserSettings_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" ref="file_label">
|
||||
<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" />
|
||||
</label>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1289,36 +1357,37 @@ module.exports = React.createClass({
|
|||
</div> : null
|
||||
}
|
||||
|
||||
{accountJsx}
|
||||
{ accountJsx }
|
||||
</div>
|
||||
|
||||
{this._renderReferral()}
|
||||
{ this._renderReferral() }
|
||||
|
||||
{notificationArea}
|
||||
{ notificationArea }
|
||||
|
||||
{this._renderUserInterfaceSettings()}
|
||||
{this._renderLabs()}
|
||||
{this._renderWebRtcSettings()}
|
||||
{this._renderDevicesPanel()}
|
||||
{this._renderCryptoInfo()}
|
||||
{this._renderBulkOptions()}
|
||||
{this._renderBugReport()}
|
||||
{ this._renderUserInterfaceSettings() }
|
||||
{ this._renderLabs() }
|
||||
{ this._renderWebRtcSettings() }
|
||||
{ this._renderDevicesPanel() }
|
||||
{ this._renderCryptoInfo() }
|
||||
{ this._renderIgnoredUsers() }
|
||||
{ this._renderBulkOptions() }
|
||||
{ this._renderBugReport() }
|
||||
|
||||
{PlatformPeg.get().isElectron() && this._renderElectronSettings()}
|
||||
{ PlatformPeg.get().isElectron() && this._renderElectronSettings() }
|
||||
|
||||
{this._renderAnalyticsControl()}
|
||||
{ this._renderAnalyticsControl() }
|
||||
|
||||
<h3>{ _t("Advanced") }</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t("Logged in as:") } {this._me}
|
||||
{ _t("Logged in as:") } { this._me }
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{_t('Access Token:')}
|
||||
{ _t('Access Token:') }
|
||||
<span className="mx_UserSettings_advanced_spoiler"
|
||||
onClick={this._showSpoiler}
|
||||
data-spoiler={ MatrixClientPeg.get().getAccessToken() }>
|
||||
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||
<{ _t("click to reveal") }>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -1329,23 +1398,23 @@ module.exports = React.createClass({
|
|||
{ _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
|
||||
</div>
|
||||
<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)
|
||||
: REACT_SDK_VERSION
|
||||
}<br/>
|
||||
{_t('riot-web version:')} {(this.state.vectorVersion !== undefined)
|
||||
}<br />
|
||||
{ _t('riot-web version:') } { (this.state.vectorVersion !== undefined)
|
||||
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
||||
: 'unknown'
|
||||
}<br/>
|
||||
{ _t("olm version:") } {olmVersionString}<br/>
|
||||
}<br />
|
||||
{ _t("olm version:") } { olmVersionString }<br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this._renderCheckUpdate()}
|
||||
{ this._renderCheckUpdate() }
|
||||
|
||||
{this._renderClearCache()}
|
||||
{ this._renderClearCache() }
|
||||
|
||||
{this._renderDeactivateAccount()}
|
||||
{ this._renderDeactivateAccount() }
|
||||
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -136,16 +137,15 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
this.setState({
|
||||
enteredHomeserverUrl: newHsUrl
|
||||
});
|
||||
},
|
||||
|
||||
onIsUrlChanged: function(newIsUrl) {
|
||||
this.setState({
|
||||
enteredIdentityServerUrl: newIsUrl
|
||||
});
|
||||
onServerConfigChange: function(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHomeserverUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
showErrorDialog: function(body, title) {
|
||||
|
@ -170,7 +170,7 @@ module.exports = React.createClass({
|
|||
else if (this.state.progress === "sent_email") {
|
||||
resetPasswordJsx = (
|
||||
<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 />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
value={ _t('I have verified my email address') } />
|
||||
|
@ -221,8 +221,7 @@ module.exports = React.createClass({
|
|||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={0}/>
|
||||
<div className="mx_Login_error">
|
||||
</div>
|
||||
|
|
|
@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var AvatarLogic = require("../../../Avatar");
|
||||
import React from 'react';
|
||||
import AvatarLogic from '../../../Avatar';
|
||||
import sdk from '../../../index';
|
||||
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
|
||||
limitations under the License.
|
||||
*/
|
||||
var React = require('react');
|
||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Avatar = require('../../../Avatar');
|
||||
var sdk = require("../../../index");
|
||||
import React from "react";
|
||||
import {ContentRepo} from "matrix-js-sdk";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import sdk from "../../../index";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
@ -30,7 +29,7 @@ module.exports = React.createClass({
|
|||
oobData: React.PropTypes.object,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string
|
||||
resizeMethod: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -44,13 +43,13 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
urls: this.getImageUrls(this.props)
|
||||
urls: this.getImageUrls(this.props),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps)
|
||||
urls: this.getImageUrls(newProps),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -61,11 +60,10 @@ module.exports = React.createClass({
|
|||
props.oobData.avatarUrl,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod
|
||||
props.resizeMethod,
|
||||
), // highest priority
|
||||
this.getRoomAvatarUrl(props),
|
||||
this.getOneToOneAvatar(props),
|
||||
this.getFallbackAvatar(props) // lowest priority
|
||||
this.getOneToOneAvatar(props), // lowest priority
|
||||
].filter(function(url) {
|
||||
return (url != null && url != "");
|
||||
});
|
||||
|
@ -79,17 +77,17 @@ module.exports = React.createClass({
|
|||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
false,
|
||||
);
|
||||
},
|
||||
|
||||
getOneToOneAvatar: function(props) {
|
||||
if (!props.room) return null;
|
||||
|
||||
var mlist = props.room.currentState.members;
|
||||
var userIds = [];
|
||||
const mlist = props.room.currentState.members;
|
||||
const userIds = [];
|
||||
// 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)) {
|
||||
userIds.push(uid);
|
||||
}
|
||||
|
@ -99,7 +97,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (userIds.length == 2) {
|
||||
var theOtherGuy = null;
|
||||
let theOtherGuy = null;
|
||||
if (mlist[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||
theOtherGuy = mlist[userIds[1]];
|
||||
} else {
|
||||
|
@ -110,7 +108,7 @@ module.exports = React.createClass({
|
|||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
false,
|
||||
);
|
||||
} else if (userIds.length == 1) {
|
||||
return mlist[userIds[0]].getAvatarUrl(
|
||||
|
@ -118,37 +116,24 @@ module.exports = React.createClass({
|
|||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
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() {
|
||||
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 (
|
||||
<BaseAvatar {...otherProps} name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
urls={this.state.urls} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ const TRUNCATE_QUERY_LIST = 40;
|
|||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "UserPickerDialog",
|
||||
displayName: "AddressPickerDialog",
|
||||
|
||||
propTypes: {
|
||||
title: PropTypes.string.isRequired,
|
||||
|
@ -38,8 +38,14 @@ module.exports = React.createClass({
|
|||
roomId: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOfType(addressTypes)),
|
||||
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
|
||||
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() {
|
||||
|
@ -47,6 +53,8 @@ module.exports = React.createClass({
|
|||
value: "",
|
||||
focus: true,
|
||||
validAddressTypes: addressTypes,
|
||||
pickerType: 'user',
|
||||
includeSelf: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -140,10 +148,22 @@ module.exports = React.createClass({
|
|||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.state.serverSupportsUserDirectory) {
|
||||
this._doUserDirectorySearch(query);
|
||||
if (this.props.pickerType === 'user') {
|
||||
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 {
|
||||
this._doLocalSearch(query);
|
||||
console.error('Unknown pickerType', this.props.pickerType);
|
||||
}
|
||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
|
@ -185,6 +205,101 @@ module.exports = React.createClass({
|
|||
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) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
|
@ -245,17 +360,30 @@ module.exports = React.createClass({
|
|||
|
||||
_processResults: function(results, query) {
|
||||
const queryList = [];
|
||||
results.forEach((user) => {
|
||||
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
|
||||
results.forEach((result) => {
|
||||
if (result.room_id) {
|
||||
queryList.push({
|
||||
addressType: 'mx-room-id',
|
||||
address: result.room_id,
|
||||
displayName: result.name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.props.includeSelf &&
|
||||
result.user_id === MatrixClientPeg.get().credentials.userId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return objects, structure of which is defined
|
||||
// by UserAddressType
|
||||
queryList.push({
|
||||
addressType: 'mx',
|
||||
address: user.user_id,
|
||||
displayName: user.display_name,
|
||||
avatarMxc: user.avatar_url,
|
||||
addressType: 'mx-user-id',
|
||||
address: result.user_id,
|
||||
displayName: result.display_name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
});
|
||||
|
@ -291,16 +419,23 @@ module.exports = React.createClass({
|
|||
address: addressText,
|
||||
isKnown: false,
|
||||
};
|
||||
if (addrType == null) {
|
||||
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||
this.setState({ error: true });
|
||||
return null;
|
||||
} else if (addrType == 'mx') {
|
||||
} else if (addrType == 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
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();
|
||||
|
@ -360,7 +495,7 @@ module.exports = React.createClass({
|
|||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
for (let i = 0; i < this.state.userList.length; i++) {
|
||||
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 addressSelector;
|
||||
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">
|
||||
{_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>;
|
||||
} 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 (
|
||||
this.state.query.length > 0 &&
|
||||
this.state.queryList.length === 0 &&
|
||||
!this.state.busy
|
||||
) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
|
||||
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
||||
} else {
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={ this.state.queryList }
|
||||
onSelected={ this.onSelected }
|
||||
truncateAt={ TRUNCATE_QUERY_LIST }
|
||||
addressList={this.state.queryList}
|
||||
onSelected={this.onSelected}
|
||||
truncateAt={TRUNCATE_QUERY_LIST}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -406,7 +554,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
{ this.props.title }
|
||||
</div>
|
||||
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
||||
onClick={this.onCancel} >
|
||||
|
@ -422,7 +570,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
||||
{this.props.button}
|
||||
{ this.props.button }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -52,20 +52,20 @@ export default React.createClass({
|
|||
|
||||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
onEnterPressed={this.onOk}
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
{ _t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.") }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass} onClick={this.onOk}>
|
||||
{_t("Remove")}
|
||||
{ _t("Remove") }
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
|
||||
/*
|
||||
* A dialog for confirming an operation on another user.
|
||||
|
@ -30,7 +31,10 @@ import classnames from 'classnames';
|
|||
export default React.createClass({
|
||||
displayName: 'ConfirmUserActionDialog',
|
||||
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'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
|
@ -69,6 +73,7 @@ export default React.createClass({
|
|||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
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 confirmButtonClass = classnames({
|
||||
|
@ -83,7 +88,7 @@ export default React.createClass({
|
|||
<form onSubmit={this.onOk}>
|
||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
||||
ref={this._collectReasonField}
|
||||
placeholder={ _t("Reason") }
|
||||
placeholder={_t("Reason")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</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 (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
onEnterPressed={this.onOk}
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
||||
{ avatar }
|
||||
</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||
</div>
|
||||
{reasonBox}
|
||||
{ reasonBox }
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass}
|
||||
onClick={this.onOk} autoFocus={!this.props.askReason}
|
||||
>
|
||||
{this.props.action}
|
||||
{ this.props.action }
|
||||
</button>
|
||||
|
||||
<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
|
||||
// any yet.
|
||||
createErrorNode = <div className="error">
|
||||
<div>{_t('Room creation failed')}</div>
|
||||
<div>{this.state.createError.message}</div>
|
||||
<div>{ _t('Room creation failed') }</div>
|
||||
<div>{ this.state.createError.message }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -156,7 +156,7 @@ export default React.createClass({
|
|||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupname">{_t('Group Name')}</label>
|
||||
<label htmlFor="groupname">{ _t('Group Name') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||
|
@ -169,7 +169,7 @@ export default React.createClass({
|
|||
</div>
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupid">{_t('Group ID')}</label>
|
||||
<label htmlFor="groupid">{ _t('Group ID') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupid" className="mx_CreateGroupDialog_input"
|
||||
|
@ -182,9 +182,9 @@ export default React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
<div className="error">
|
||||
{this.state.groupIdError}
|
||||
{ this.state.groupIdError }
|
||||
</div>
|
||||
{createErrorNode}
|
||||
{ createErrorNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onCancel}>
|
||||
|
|
|
@ -28,25 +28,25 @@ export default function DeviceVerifyDialog(props) {
|
|||
const body = (
|
||||
<div>
|
||||
<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) " +
|
||||
"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>
|
||||
<div className="mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<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 key")}:</label> <span><code><b>{ key }</b></code></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 key") }:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<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 " +
|
||||
"and you probably want to press the blacklist button instead.")}
|
||||
"and you probably want to press the blacklist button instead.") }
|
||||
</p>
|
||||
<p>
|
||||
{_t("In future this verification process will be more sophisticated.")}
|
||||
{ _t("In future this verification process will be more sophisticated.") }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -63,11 +63,11 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title || _t('Error')}>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description || _t('An error has occurred.')}
|
||||
{ this.props.description || _t('An error has occurred.') }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
||||
{this.props.button || _t('OK')}
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -18,7 +18,7 @@ import Modal from '../../../Modal';
|
|||
import React from 'react';
|
||||
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
|
||||
|
@ -116,27 +116,27 @@ export default React.createClass({
|
|||
|
||||
let text;
|
||||
if (this.state.wasNewDevice) {
|
||||
text = "You added a new device '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.";
|
||||
text = _td("You added a new device '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.");
|
||||
} else {
|
||||
text = "Your unverified device '%(displayName)s' is requesting"
|
||||
+ " encryption keys.";
|
||||
text = _td("Your unverified device '%(displayName)s' is requesting"
|
||||
+ " encryption keys.");
|
||||
}
|
||||
text = _t(text, {displayName: displayName});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{text}</p>
|
||||
<p>{ text }</p>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onVerifyClicked}>
|
||||
{_t('Start verification')}
|
||||
{ _t('Start verification') }
|
||||
</button>
|
||||
<button onClick={this._onShareClicked}>
|
||||
{_t('Share without verifying')}
|
||||
{ _t('Share without verifying') }
|
||||
</button>
|
||||
<button onClick={this._onIgnoreClicked}>
|
||||
{_t('Ignore request')}
|
||||
{ _t('Ignore request') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -154,7 +154,7 @@ export default React.createClass({
|
|||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<p>{_t('Loading device info...')}</p>
|
||||
<p>{ _t('Loading device info...') }</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
@ -165,7 +165,7 @@ export default React.createClass({
|
|||
onFinished={this.props.onFinished}
|
||||
title={_t('Encryption key request')}
|
||||
>
|
||||
{content}
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,6 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'QuestionDialog',
|
||||
|
@ -25,6 +27,7 @@ export default React.createClass({
|
|||
description: React.PropTypes.node,
|
||||
extraButtons: React.PropTypes.node,
|
||||
button: React.PropTypes.string,
|
||||
danger: React.PropTypes.bool,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
@ -36,6 +39,7 @@ export default React.createClass({
|
|||
extraButtons: null,
|
||||
focus: true,
|
||||
hasCancelButton: true,
|
||||
danger: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -51,23 +55,27 @@ export default React.createClass({
|
|||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const cancelButton = this.props.hasCancelButton ? (
|
||||
<button onClick={this.onCancel}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
) : null;
|
||||
const buttonClasses = classnames({
|
||||
mx_Dialog_primary: true,
|
||||
danger: this.props.danger,
|
||||
});
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{this.props.button || _t('OK')}
|
||||
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
{this.props.extraButtons}
|
||||
{cancelButton}
|
||||
{ this.props.extraButtons }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -45,10 +45,10 @@ export default React.createClass({
|
|||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugreport = (
|
||||
<p>
|
||||
{_tJsx(
|
||||
{ _tJsx(
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
@ -57,19 +57,19 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={_t('Unable to restore session')}>
|
||||
<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 " +
|
||||
"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 " +
|
||||
"to the more recent version.")}</p>
|
||||
"to the more recent version.") }</p>
|
||||
|
||||
{bugreport}
|
||||
{ bugreport }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
||||
{_t("Continue anyway")}
|
||||
{ _t("Continue anyway") }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -130,10 +130,10 @@ export default React.createClass({
|
|||
|
||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||
className="mx_SetEmailDialog_email_input"
|
||||
placeholder={ _t("Email address") }
|
||||
placeholder={_t("Email address")}
|
||||
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this.onEmailAddressChanged } />;
|
||||
blurToCancel={false}
|
||||
onValueChanged={this.onEmailAddressChanged} />;
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_SetEmailDialog"
|
||||
|
|
|
@ -226,7 +226,7 @@ export default React.createClass({
|
|||
let usernameIndicator = null;
|
||||
let usernameBusyIndicator = null;
|
||||
if (this.state.usernameBusy) {
|
||||
usernameBusyIndicator = <Spinner w="24" h="24"/>;
|
||||
usernameBusyIndicator = <Spinner w="24" h="24" />;
|
||||
} else {
|
||||
const usernameAvailable = this.state.username &&
|
||||
this.state.usernameCheckSupport && !this.state.usernameError;
|
||||
|
@ -275,17 +275,17 @@ export default React.createClass({
|
|||
/<a>(.*?)<\/a>/,
|
||||
],
|
||||
[
|
||||
(sub) => <span>{this.props.homeserverUrl}</span>,
|
||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
|
||||
(sub) => <span>{ this.props.homeserverUrl }</span>,
|
||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||
],
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _tJsx(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
/<a>(.*?)<\/a>/,
|
||||
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
|
||||
)}
|
||||
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a>],
|
||||
) }
|
||||
</p>
|
||||
{ auth }
|
||||
{ authErrorIndicator }
|
||||
|
|
|
@ -65,10 +65,10 @@ export default React.createClass({
|
|||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_TextInputDialog_label">
|
||||
<label htmlFor="textinput"> {this.props.description} </label>
|
||||
<label htmlFor="textinput"> { this.props.description } </label>
|
||||
</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 className="mx_Dialog_buttons">
|
||||
|
@ -76,7 +76,7 @@ export default React.createClass({
|
|||
{ _t("Cancel") }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||
{this.props.button}
|
||||
{ this.props.button }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
|||
import AccessibleButton from './AccessibleButton';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'RoleButton',
|
||||
|
@ -47,6 +48,7 @@ export default React.createClass({
|
|||
|
||||
_onClick: function(ev) {
|
||||
ev.stopPropagation();
|
||||
Analytics.trackEvent('Action Button', 'click', this.props.action);
|
||||
dis.dispatch({action: this.props.action});
|
||||
},
|
||||
|
||||
|
|
|
@ -45,11 +45,12 @@ export default React.createClass({
|
|||
const address = this.props.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(
|
||||
address.avatarMxc, 25, 25, 'crop'
|
||||
address.avatarMxc, 25, 25, 'crop',
|
||||
));
|
||||
} else if (address.addressType === 'email') {
|
||||
imgUrls.push('img/icon-email-user.svg');
|
||||
|
@ -77,7 +78,7 @@ export default React.createClass({
|
|||
|
||||
let info;
|
||||
let error = false;
|
||||
if (address.addressType === "mx" && address.isKnown) {
|
||||
if (isMatrixAddress && address.isKnown) {
|
||||
const idClasses = classNames({
|
||||
"mx_AddressTile_id": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
|
@ -89,7 +90,7 @@ export default React.createClass({
|
|||
<div className={idClasses}>{ address.address }</div>
|
||||
</div>
|
||||
);
|
||||
} else if (address.addressType === "mx") {
|
||||
} else if (isMatrixAddress) {
|
||||
const unknownMxClasses = classNames({
|
||||
"mx_AddressTile_unknownMx": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
|
|
|
@ -47,13 +47,19 @@ export default class AppPermission extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
let e2eWarningText;
|
||||
if (this.props.isRoomEncrypted) {
|
||||
e2eWarningText =
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
|
||||
}
|
||||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||
</div>
|
||||
<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>
|
||||
<input
|
||||
className='mx_AppPermissionButton'
|
||||
|
@ -67,9 +73,11 @@ export default class AppPermission extends React.Component {
|
|||
}
|
||||
|
||||
AppPermission.propTypes = {
|
||||
isRoomEncrypted: PropTypes.bool,
|
||||
url: PropTypes.string.isRequired,
|
||||
onPermissionGranted: PropTypes.func.isRequired,
|
||||
};
|
||||
AppPermission.defaultProps = {
|
||||
isRoomEncrypted: false,
|
||||
onPermissionGranted: function() {},
|
||||
};
|
||||
|
|
|
@ -19,18 +19,19 @@ limitations under the License.
|
|||
import url from 'url';
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
import MessageSpinner from './MessageSpinner';
|
||||
import WidgetUtils from '../../../WidgetUtils';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
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({
|
||||
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.
|
||||
// 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,
|
||||
// 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() {
|
||||
|
@ -59,7 +64,8 @@ export default React.createClass({
|
|||
loading: false,
|
||||
widgetUrl: this.props.url,
|
||||
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,
|
||||
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
|
||||
isScalarUrl: function() {
|
||||
const scalarUrl = SdkConfig.get().integrations_rest_url;
|
||||
return scalarUrl && this.props.url.startsWith(scalarUrl);
|
||||
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
|
||||
if (!scalarUrls || scalarUrls.length == 0) {
|
||||
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
||||
}
|
||||
|
||||
for (let i = 0; i < scalarUrls.length; i++) {
|
||||
if (this.props.url.startsWith(scalarUrls[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isMixedContent: function() {
|
||||
|
@ -113,6 +128,30 @@ export default React.createClass({
|
|||
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() {
|
||||
|
@ -122,7 +161,8 @@ export default React.createClass({
|
|||
_onEditClick: function(e) {
|
||||
console.log("Edit widget ID ", this.props.id);
|
||||
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, {
|
||||
src: src,
|
||||
}, "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
|
||||
_deleteWidgetLabel() {
|
||||
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 */
|
||||
|
@ -177,11 +217,25 @@ export default React.createClass({
|
|||
let appTileName = "No name";
|
||||
if(this.props.name && this.props.name.trim()) {
|
||||
appTileName = this.props.name.trim();
|
||||
appTileName = appTileName[0].toUpperCase() + appTileName.slice(1).toLowerCase();
|
||||
}
|
||||
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() {
|
||||
let appTileBody;
|
||||
|
||||
|
@ -203,42 +257,46 @@ export default React.createClass({
|
|||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
|
||||
if (this.state.loading) {
|
||||
appTileBody = (
|
||||
<div className='mx_AppTileBody mx_AppLoading'>
|
||||
<MessageSpinner msg='Loading...'/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.hasPermissionToLoad == true) {
|
||||
if (this.isMixedContent()) {
|
||||
if (this.props.show) {
|
||||
if (this.state.loading) {
|
||||
appTileBody = (
|
||||
<div className='mx_AppTileBody mx_AppLoading'>
|
||||
<MessageSpinner msg='Loading...' />
|
||||
</div>
|
||||
);
|
||||
} 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 = (
|
||||
<div className="mx_AppTileBody">
|
||||
<AppWarning
|
||||
errorMsg="Error - Mixed content"
|
||||
<AppPermission
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
url={this.state.widgetUrl}
|
||||
onPermissionGranted={this._grantWidgetPermission}
|
||||
/>
|
||||
</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
|
||||
|
@ -253,21 +311,20 @@ export default React.createClass({
|
|||
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div className="mx_AppTileMenuBar">
|
||||
{this.formatAppTileName()}
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
{ this.formatAppTileName() }
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
<span className="mx_Beta" alt={betaHelpMsg} title={betaHelpMsg}>β</span>
|
||||
{/* Edit widget */}
|
||||
{showEditButton && <img
|
||||
{ /* Edit widget */ }
|
||||
{ showEditButton && <img
|
||||
src="img/edit.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
width="8" height="8"
|
||||
alt={_t('Edit')}
|
||||
title={_t('Edit')}
|
||||
onClick={this._onEditClick}
|
||||
/>}
|
||||
/> }
|
||||
|
||||
{/* Delete widget */}
|
||||
{ /* Delete widget */ }
|
||||
<img src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
width="8" height="8"
|
||||
|
@ -277,7 +334,7 @@ export default React.createClass({
|
|||
/>
|
||||
</span>
|
||||
</div>
|
||||
{appTileBody}
|
||||
{ appTileBody }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -6,10 +6,10 @@ const AppWarning = (props) => {
|
|||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{props.errorMsg}</span>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
|
||||
</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) {
|
||||
if (nextProps.initialValue !== this.props.initialValue) {
|
||||
if (nextProps.initialValue !== this.props.initialValue ||
|
||||
nextProps.initialValue !== this.value
|
||||
) {
|
||||
this.value = nextProps.initialValue;
|
||||
if (this.refs.editable_div) {
|
||||
this.showPlaceholder(!this.value);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,12 +16,19 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||
|
||||
export default function EmojiText(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 = {
|
||||
|
|
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,
|
||||
// Called when the MELS expansion is toggled
|
||||
onToggle: React.PropTypes.func,
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
expanded: false,
|
||||
expanded: Boolean(this.props.startExpanded),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -376,7 +378,7 @@ module.exports = React.createClass({
|
|||
return items[0];
|
||||
} else if (remaining) {
|
||||
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 one other", { items: items.join(', ') });
|
||||
} else {
|
||||
|
|
|
@ -26,8 +26,8 @@ module.exports = React.createClass({
|
|||
const msg = this.props.msg || "Loading...";
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
<div className="mx_Spinner_Msg">{msg}</div>
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
|
||||
<div className="mx_Spinner_Msg">{ msg }</div>
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -167,11 +167,11 @@ const Pill = React.createClass({
|
|||
userId = member.userId;
|
||||
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <MemberAvatar member={member} width={16} height={16}/>;
|
||||
avatar = <MemberAvatar member={member} width={16} height={16} />;
|
||||
}
|
||||
pillClass = 'mx_UserPill';
|
||||
href = null;
|
||||
onClick = this.onUserPillClicked.bind(this);
|
||||
onClick = this.onUserPillClicked;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -180,7 +180,7 @@ const Pill = React.createClass({
|
|||
if (room) {
|
||||
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <RoomAvatar room={room} width={16} height={16}/>;
|
||||
avatar = <RoomAvatar room={room} width={16} height={16} />;
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
|
@ -195,12 +195,12 @@ const Pill = React.createClass({
|
|||
if (this.state.pillType) {
|
||||
return this.props.inMessage ?
|
||||
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
|
||||
{avatar}
|
||||
{linkText}
|
||||
{ avatar }
|
||||
{ linkText }
|
||||
</a> :
|
||||
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
||||
{avatar}
|
||||
{linkText}
|
||||
{ avatar }
|
||||
{ linkText }
|
||||
</span>;
|
||||
} else {
|
||||
// Deliberately render nothing if the URL isn't recognised
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -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
|
||||
limitations under the License.
|
||||
*/
|
||||
var React = require('react');
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -21,12 +24,21 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// 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
|
||||
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.
|
||||
// This will be inserted after the children.
|
||||
createOverflowElement: React.PropTypes.func
|
||||
createOverflowElement: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -34,40 +46,56 @@ module.exports = React.createClass({
|
|||
truncateAt: 2,
|
||||
createOverflowElement: function(overflowCount, totalCount) {
|
||||
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() {
|
||||
var childsJsx = this.props.children;
|
||||
var overflowJsx;
|
||||
var childArray = React.Children.toArray(this.props.children).filter((c) => {
|
||||
return c != null;
|
||||
});
|
||||
|
||||
var childCount = childArray.length;
|
||||
let overflowNode = null;
|
||||
|
||||
const totalChildren = this._getChildCount();
|
||||
let upperBound = totalChildren;
|
||||
if (this.props.truncateAt >= 0) {
|
||||
var overflowCount = childCount - this.props.truncateAt;
|
||||
|
||||
const overflowCount = totalChildren - this.props.truncateAt;
|
||||
if (overflowCount > 1) {
|
||||
overflowJsx = this.props.createOverflowElement(
|
||||
overflowCount, childCount
|
||||
overflowNode = this.props.createOverflowElement(
|
||||
overflowCount, totalChildren,
|
||||
);
|
||||
|
||||
// cut out the overflow elements
|
||||
childArray.splice(childCount - overflowCount, overflowCount);
|
||||
childsJsx = childArray; // use what is left
|
||||
upperBound = this.props.truncateAt;
|
||||
}
|
||||
}
|
||||
const childNodes = this._getChildren(0, upperBound);
|
||||
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
{childsJsx}
|
||||
{overflowJsx}
|
||||
{ childNodes }
|
||||
{ overflowNode }
|
||||
</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';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
|
||||
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)
|
||||
// * embedding the captcha in an iframe (if that works)
|
||||
// * using a better captcha lib
|
||||
warning.innerHTML = _tJsx(
|
||||
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return "<a href='https://riot.im/app'>{ sub }</a>"; }
|
||||
);
|
||||
ReactDOM.render(_tJsx(
|
||||
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning);
|
||||
this.refs.recaptchaContainer.appendChild(warning);
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -25,7 +25,7 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import Flair from '../elements/Flair.js';
|
||||
|
||||
export default function SenderProfile(props) {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
@ -30,8 +31,17 @@ export default function SenderProfile(props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<EmojiText className="mx_SenderProfile" dir="auto"
|
||||
onClick={props.onClick}>{`${name || ''} ${props.aux || ''}`}</EmojiText>
|
||||
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
|
||||
<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 UserSettingsStore from "../../../UserSettingsStore";
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import ContextualMenu from '../../structures/ContextualMenu';
|
||||
import {RoomMember} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -72,12 +73,16 @@ module.exports = React.createClass({
|
|||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
|
||||
let successful = false;
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
successful = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.log('Unable to copy');
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
return successful;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -113,14 +118,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}, 10);
|
||||
}
|
||||
// add event handlers to the 'copy code' buttons
|
||||
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);
|
||||
};
|
||||
}
|
||||
this._addCodeCopyButton();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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) {
|
||||
this.setState({ widgetHidden: true });
|
||||
// FIXME: persist this somewhere smarter than local storage
|
||||
|
|
|
@ -23,10 +23,15 @@ module.exports = React.createClass({
|
|||
displayName: 'UnknownBody',
|
||||
|
||||
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;
|
||||
return (
|
||||
<span className="mx_UnknownBody" title={_t("Removed or unknown message type")}>
|
||||
{text}
|
||||
<span className="mx_UnknownBody" title={tooltip}>
|
||||
{ text }
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -136,24 +136,25 @@ module.exports = React.createClass({
|
|||
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 (this.isAliasValid(alias)) {
|
||||
// add this alias to the domain to aliases dict
|
||||
var domain = alias.replace(/^.*?:/, '');
|
||||
// XXX: do we need to deep copy aliases before editing it?
|
||||
this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || [];
|
||||
this.state.domainToAliases[domain].push(alias);
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases
|
||||
});
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || [];
|
||||
this.state.domainToAliases[localDomain].push(alias);
|
||||
|
||||
// reset the add field
|
||||
this.refs.add_alias.setValue(''); // FIXME
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases,
|
||||
// Reset the add field
|
||||
newAlias: "",
|
||||
});
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
|
||||
title: _t('Invalid alias format'),
|
||||
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
|
||||
var oldAlias;
|
||||
if (this.isAliasValid(alias)) {
|
||||
oldAlias = this.state.domainToAliases[domain][index];
|
||||
this.state.domainToAliases[domain][index] = alias;
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain][index] = alias;
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
|
||||
title: _t('Invalid address format'),
|
||||
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
|
||||
// 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
|
||||
// would be to arbitrarily deepcopy to a temp variable and then setState
|
||||
// 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({
|
||||
domainToAliases: this.state.domainToAliases
|
||||
domainToAliases: this.state.domainToAliases,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -198,6 +198,7 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
var self = this;
|
||||
var EditableText = sdk.getComponent("elements.EditableText");
|
||||
var EditableItemList = sdk.getComponent("elements.EditableItemList");
|
||||
var localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
var canonical_alias_section;
|
||||
|
@ -257,58 +258,24 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomSettings_aliasLabel">
|
||||
{ _t('The main address for this room is') }: { canonical_alias_section }
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
{ (this.state.domainToAliases[localDomain] &&
|
||||
this.state.domainToAliases[localDomain].length > 0)
|
||||
? _t('Local addresses for this room:')
|
||||
: _t('This room has no local addresses') }
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasesTable">
|
||||
{ (this.state.domainToAliases[localDomain] || []).map((alias, i) => {
|
||||
var deleteButton;
|
||||
if (this.props.canSetAliases) {
|
||||
deleteButton = (
|
||||
<img src="img/cancel-small.svg" width="14" height="14"
|
||||
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>
|
||||
<EditableItemList
|
||||
className={"mx_RoomSettings_localAliases"}
|
||||
items={this.state.domainToAliases[localDomain] || []}
|
||||
newItem={this.state.newAlias}
|
||||
onNewItemChanged={this.onNewAliasChanged}
|
||||
onItemAdded={this.onLocalAliasAdded}
|
||||
onItemEdited={this.onLocalAliasChanged}
|
||||
onItemRemoved={this.onLocalAliasDeleted}
|
||||
itemsLabel={_t('Local addresses for this room:')}
|
||||
noItemsLabel={_t('This room has no local addresses')}
|
||||
placeholder={_t(
|
||||
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
|
||||
)}
|
||||
/>
|
||||
|
||||
{ remote_aliases_section }
|
||||
|
||||
</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 WidgetUtils from '../../../WidgetUtils';
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
const MAX_WIDGETS = 2;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'AppsDrawer',
|
||||
|
@ -51,19 +53,18 @@ module.exports = React.createClass({
|
|||
this.scalarClient = null;
|
||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.scalarClient.connect().then(() => {
|
||||
this.forceUpdate();
|
||||
if (this.state.apps && this.state.apps.length < 1) {
|
||||
this.onClickAddWidget();
|
||||
}
|
||||
// TODO -- Handle Scalar errors
|
||||
// },
|
||||
// (err) => {
|
||||
// this.setState({
|
||||
// scalar_error: err,
|
||||
// });
|
||||
}).catch((e) => {
|
||||
console.log("Failed to connect to integrations server");
|
||||
// TODO -- Handle Scalar errors
|
||||
// this.setState({
|
||||
// scalar_error: err,
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -71,6 +72,27 @@ module.exports = React.createClass({
|
|||
if (MatrixClientPeg.get()) {
|
||||
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;
|
||||
},
|
||||
|
||||
_initAppConfig: function(appId, app) {
|
||||
_initAppConfig: function(appId, app, sender) {
|
||||
const user = MatrixClientPeg.get().getUser(this.props.userId);
|
||||
const params = {
|
||||
'$matrix_user_id': this.props.userId,
|
||||
|
@ -111,6 +133,7 @@ module.exports = React.createClass({
|
|||
app.id = appId;
|
||||
app.name = app.name || app.type;
|
||||
app.url = this.encodeUri(app.url, params);
|
||||
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
|
||||
|
||||
return app;
|
||||
},
|
||||
|
@ -131,18 +154,12 @@ module.exports = React.createClass({
|
|||
return appsStateEvents.filter((ev) => {
|
||||
return ev.getContent().type && ev.getContent().url;
|
||||
}).map((ev) => {
|
||||
return this._initAppConfig(ev.getStateKey(), ev.getContent());
|
||||
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
||||
});
|
||||
},
|
||||
|
||||
_updateApps: function() {
|
||||
const apps = this._getApps();
|
||||
if (apps.length < 1) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
apps: apps,
|
||||
});
|
||||
|
@ -157,11 +174,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onClickAddWidget: function(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
_launchManageIntegrations: function() {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
|
||||
|
@ -171,6 +184,23 @@ module.exports = React.createClass({
|
|||
}, "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() {
|
||||
const apps = this.state.apps.map(
|
||||
(app, index, arr) => {
|
||||
|
@ -183,24 +213,34 @@ module.exports = React.createClass({
|
|||
fullWidth={arr.length<2 ? true : false}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
show={this.props.showApps}
|
||||
creatorUserId={app.creatorUserId}
|
||||
/>);
|
||||
});
|
||||
|
||||
const addWidget = this.state.apps && this.state.apps.length < 2 && this._canUserModify() &&
|
||||
(<div onClick={this.onClickAddWidget}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
className="mx_AddWidget_button"
|
||||
title={_t('Add a widget')}>
|
||||
[+] {_t('Add a widget')}
|
||||
</div>);
|
||||
let addWidget;
|
||||
if (this.props.showApps &&
|
||||
this._canUserModify()
|
||||
) {
|
||||
addWidget = <div
|
||||
onClick={this.onClickAddWidget}
|
||||
role="button"
|
||||
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 (
|
||||
<div className="mx_AppsDrawer">
|
||||
<div id="apps" className="mx_AppsContainer">
|
||||
{apps}
|
||||
{ apps }
|
||||
</div>
|
||||
{addWidget}
|
||||
{ this._canUserModify() && addWidget }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -143,7 +143,6 @@ export default class Autocomplete extends React.Component {
|
|||
return null;
|
||||
}
|
||||
this.setSelection(selectionOffset);
|
||||
return selectionOffset === COMPOSER_SELECTED ? null : this.state.completionList[selectionOffset - 1];
|
||||
}
|
||||
|
||||
// called from MessageComposerInput
|
||||
|
@ -155,7 +154,6 @@ export default class Autocomplete extends React.Component {
|
|||
return null;
|
||||
}
|
||||
this.setSelection(selectionOffset);
|
||||
return selectionOffset === COMPOSER_SELECTED ? null : this.state.completionList[selectionOffset - 1];
|
||||
}
|
||||
|
||||
onEscape(e): boolean {
|
||||
|
@ -201,6 +199,9 @@ export default class Autocomplete extends React.Component {
|
|||
|
||||
setSelection(selectionOffset: number) {
|
||||
this.setState({selectionOffset, hide: false});
|
||||
if (this.props.onSelectionChange) {
|
||||
this.props.onSelectionChange(this.state.completionList[selectionOffset - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
|
|
@ -129,11 +129,13 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
let appsDrawer = null;
|
||||
if(UserSettingsStore.isFeatureEnabled('matrix_apps') && this.props.showApps) {
|
||||
if(UserSettingsStore.isFeatureEnabled('matrix_apps')) {
|
||||
appsDrawer = <AppsDrawer ref="appsDrawer"
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}/>;
|
||||
maxHeight={this.props.maxHeight}
|
||||
showApps={this.props.showApps}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -44,6 +44,8 @@ var eventTileTypes = {
|
|||
'm.room.history_visibility' : 'messages.TextualEvent',
|
||||
'm.room.encryption' : 'messages.TextualEvent',
|
||||
'm.room.power_levels' : 'messages.TextualEvent',
|
||||
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
var MAX_READ_AVATARS = 5;
|
||||
|
@ -506,10 +508,10 @@ module.exports = withMatrixClient(React.createClass({
|
|||
if (msgtype === 'm.image') aux = _t('sent an image');
|
||||
else if (msgtype === 'm.video') aux = _t('sent a video');
|
||||
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 {
|
||||
sender = <SenderProfile mxEvent={this.props.mxEvent} />;
|
||||
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={true} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -62,6 +62,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
updating: 0,
|
||||
devicesLoading: true,
|
||||
devices: null,
|
||||
isIgnoring: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -81,6 +82,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("accountData", this.onAccountData);
|
||||
|
||||
this._checkIgnoreState();
|
||||
},
|
||||
|
||||
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) {
|
||||
var names = Object.create(null);
|
||||
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() {
|
||||
const membership = this.props.member.membership;
|
||||
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() {
|
||||
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
|
||||
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
|
||||
|
@ -708,7 +751,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
if (kickButton || banButton || muteButton || giveModButton) {
|
||||
adminTools =
|
||||
<div>
|
||||
<h3>{_t("Admin tools")}</h3>
|
||||
<h3>{_t("Admin Tools")}</h3>
|
||||
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{muteButton}
|
||||
|
@ -756,6 +799,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{ this._renderUserOptions() }
|
||||
|
||||
{ adminTools }
|
||||
|
||||
{ startChat }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -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
|
||||
limitations under the License.
|
||||
*/
|
||||
var React = require('react');
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
var classNames = require('classnames');
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
import Promise from 'bluebird';
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
var Entities = require("../../../Entities");
|
||||
var sdk = require('../../../index');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var rate_limited_func = require('../../../ratelimitedfunc');
|
||||
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({
|
||||
displayName: 'MemberList',
|
||||
|
||||
getInitialState: function() {
|
||||
var state = {
|
||||
members: [],
|
||||
this.memberDict = this.getMemberDict();
|
||||
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
|
||||
// in practice I find that a little constraining
|
||||
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
|
||||
truncateAtJoined: INITIAL_LOAD_NUM_MEMBERS,
|
||||
truncateAtInvited: INITIAL_LOAD_NUM_INVITED,
|
||||
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() {
|
||||
|
@ -147,10 +143,12 @@ module.exports = React.createClass({
|
|||
// console.log("Updating memberlist");
|
||||
this.memberDict = this.getMemberDict();
|
||||
|
||||
var self = this;
|
||||
this.setState({
|
||||
members: self.roomMembers()
|
||||
});
|
||||
const newState = {
|
||||
members: this.roomMembers(),
|
||||
};
|
||||
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join');
|
||||
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite');
|
||||
this.setState(newState);
|
||||
}, 500),
|
||||
|
||||
getMemberDict: function() {
|
||||
|
@ -199,7 +197,15 @@ module.exports = React.createClass({
|
|||
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.
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
@ -208,13 +214,19 @@ module.exports = React.createClass({
|
|||
<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} />
|
||||
onClick={onClick} />
|
||||
);
|
||||
},
|
||||
|
||||
_showFullMemberList: function() {
|
||||
_showMoreJoinedMemberList: function() {
|
||||
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) {
|
||||
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) {
|
||||
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
query = (query || "").toLowerCase();
|
||||
|
||||
var self = this;
|
||||
|
||||
var memberList = self.state.members.filter(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
_filterMembers: function(members, membership, query) {
|
||||
return members.filter((userId) => {
|
||||
const m = this.memberDict[userId];
|
||||
|
||||
if (query) {
|
||||
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
|
||||
|
@ -302,14 +314,23 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
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 (
|
||||
<MemberTile key={userId} member={m} ref={userId} />
|
||||
);
|
||||
});
|
||||
|
||||
// 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") {
|
||||
// include 3pid invites (m.room.third_party_invite) state events.
|
||||
// The HS may have already converted these into m.room.member invites so
|
||||
|
@ -333,7 +354,7 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
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;
|
||||
},
|
||||
|
||||
_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() {
|
||||
var invitedSection = null;
|
||||
var invitedMemberTiles = this.makeMemberTiles('invite', this.state.searchQuery);
|
||||
if (invitedMemberTiles.length > 0) {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
|
||||
let invitedSection = null;
|
||||
if (this._getChildCountInvited() > 0) {
|
||||
invitedSection = (
|
||||
<div className="mx_MemberList_invited">
|
||||
<h2>{ _t("Invited") }</h2>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
var inputBox = (
|
||||
const inputBox = (
|
||||
<form autoComplete="off">
|
||||
<input className="mx_MemberList_query" id="mx_MemberList_query" type="text"
|
||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||
|
@ -364,15 +406,15 @@ module.exports = React.createClass({
|
|||
</form>
|
||||
);
|
||||
|
||||
var 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.makeMemberTiles('join', this.state.searchQuery)}
|
||||
</TruncatedList>
|
||||
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined}
|
||||
/>
|
||||
{invitedSection}
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
|
|
|
@ -289,12 +289,12 @@ export default class MessageComposer extends React.Component {
|
|||
if (this.props.showApps) {
|
||||
hideAppsButton =
|
||||
<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>;
|
||||
} else {
|
||||
showAppsButton =
|
||||
<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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import SlashCommands from '../../../SlashCommands';
|
|||
import KeyCode from '../../../KeyCode';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
|
@ -949,8 +949,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
};
|
||||
|
||||
moveAutocompleteSelection = (up) => {
|
||||
const completion = up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
|
||||
return this.setDisplayedCompletion(completion);
|
||||
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
|
||||
};
|
||||
|
||||
onEscape = async (e) => {
|
||||
|
@ -1033,10 +1032,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
buttons. */
|
||||
getSelectionInfo(editorState: EditorState) {
|
||||
const styleName = {
|
||||
BOLD: 'bold',
|
||||
ITALIC: 'italic',
|
||||
STRIKETHROUGH: 'strike',
|
||||
UNDERLINE: 'underline',
|
||||
BOLD: _td('bold'),
|
||||
ITALIC: _td('italic'),
|
||||
STRIKETHROUGH: _td('strike'),
|
||||
UNDERLINE: _td('underline'),
|
||||
};
|
||||
|
||||
const originalStyle = editorState.getCurrentInlineStyle().toArray();
|
||||
|
@ -1045,10 +1044,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
.filter((styleName) => !!styleName);
|
||||
|
||||
const blockName = {
|
||||
'code-block': 'code',
|
||||
'blockquote': 'quote',
|
||||
'unordered-list-item': 'bullet',
|
||||
'ordered-list-item': 'numbullet',
|
||||
'code-block': _td('code'),
|
||||
'blockquote': _td('quote'),
|
||||
'unordered-list-item': _td('bullet'),
|
||||
'ordered-list-item': _td('numbullet'),
|
||||
};
|
||||
const originalBlockType = editorState.getCurrentContent()
|
||||
.getBlockForKey(editorState.getSelection().getStartKey())
|
||||
|
@ -1133,6 +1132,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
<Autocomplete
|
||||
ref={(e) => this.autocomplete = e}
|
||||
onConfirm={this.setDisplayedCompletion}
|
||||
onSelectionChange={this.setDisplayedCompletion}
|
||||
query={this.getAutocompleteQuery(content)}
|
||||
selection={selection}/>
|
||||
</div>
|
||||
|
|
|
@ -70,7 +70,7 @@ module.exports = React.createClass({
|
|||
if (presence === "online") return _t("Online");
|
||||
if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
|
||||
if (presence === "offline") return _t("Offline");
|
||||
return "Unknown";
|
||||
return _t("Unknown");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -123,7 +123,19 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
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 enterTransitionOpts = [];
|
||||
|
@ -131,13 +143,12 @@ module.exports = React.createClass({
|
|||
if (oldInfo && oldInfo.left) {
|
||||
// start at the old height and in the old h pos
|
||||
|
||||
var leftOffset = oldInfo.left;
|
||||
startStyles.push({ top: startTopOffset+"px",
|
||||
left: oldInfo.left+"px" });
|
||||
|
||||
var reorderTransitionOpts = {
|
||||
duration: 100,
|
||||
easing: 'easeOut'
|
||||
easing: 'easeOut',
|
||||
};
|
||||
|
||||
enterTransitionOpts.push(reorderTransitionOpts);
|
||||
|
@ -175,7 +186,7 @@ module.exports = React.createClass({
|
|||
if (this.props.timestamp) {
|
||||
title = _t(
|
||||
"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 linkifyMatrix from '../../../linkify-matrix';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ManageIntegsButton from '../elements/ManageIntegsButton';
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
@ -47,6 +48,7 @@ module.exports = React.createClass({
|
|||
onSaveClick: React.PropTypes.func,
|
||||
onSearchClick: React.PropTypes.func,
|
||||
onLeaveClick: React.PropTypes.func,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -54,6 +56,7 @@ module.exports = React.createClass({
|
|||
editing: false,
|
||||
inRoom: false,
|
||||
onSaveClick: function() {},
|
||||
onCancelClick: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -183,18 +186,18 @@ module.exports = React.createClass({
|
|||
|
||||
saveButton = (
|
||||
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
|
||||
{_t("Save")}
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick}/>;
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
|
||||
if (this.props.saving) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
|
||||
spinner = <div className="mx_RoomHeader_spinner"><Spinner /></div>;
|
||||
}
|
||||
|
||||
if (canSetRoomName) {
|
||||
|
@ -251,7 +254,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
if (topic) {
|
||||
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) {
|
||||
roomAvatar = (
|
||||
<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} />
|
||||
</div>
|
||||
<div className="mx_RoomHeader_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" ref="file_label">
|
||||
<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" />
|
||||
</label>
|
||||
<input id="avatarInput" type="file" onChange={ this.onAvatarSelected }/>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -283,7 +286,7 @@ module.exports = React.createClass({
|
|||
if (this.props.onSettingsClick) {
|
||||
settingsButton =
|
||||
<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>;
|
||||
}
|
||||
|
||||
|
@ -298,32 +301,40 @@ module.exports = React.createClass({
|
|||
let forgetButton;
|
||||
if (this.props.onForgetClick) {
|
||||
forgetButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
|
||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={_t("Forget room")}>
|
||||
<TintableSvg src="img/leave.svg" width="26" height="20" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let searchButton;
|
||||
if (this.props.onSearchClick && this.props.inRoom) {
|
||||
searchButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
|
||||
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={_t("Search")}>
|
||||
<TintableSvg src="img/icons-search.svg" width="35" height="35" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let rightPanelButtons;
|
||||
if (this.props.collapsedRhs) {
|
||||
rightPanelButtons =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={_t('Show panel')}>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
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) {
|
||||
rightRow =
|
||||
<div className="mx_RoomHeader_rightRow">
|
||||
{ settingsButton }
|
||||
{ manageIntegsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
{ rightPanelButtons }
|
||||
|
@ -331,7 +342,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
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_leftRow">
|
||||
<div className="mx_RoomHeader_avatar">
|
||||
|
@ -342,10 +353,10 @@ module.exports = React.createClass({
|
|||
{ topicElement }
|
||||
</div>
|
||||
</div>
|
||||
{spinner}
|
||||
{saveButton}
|
||||
{cancelButton}
|
||||
{rightRow}
|
||||
{ spinner }
|
||||
{ saveButton }
|
||||
{ cancelButton }
|
||||
{ rightRow }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -63,7 +63,6 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
collapsed: React.PropTypes.bool.isRequired,
|
||||
currentRoom: React.PropTypes.string,
|
||||
searchFilter: React.PropTypes.string,
|
||||
},
|
||||
|
||||
|
@ -88,7 +87,9 @@ module.exports = React.createClass({
|
|||
cli.on("Room.receipt", this.onRoomReceipt);
|
||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("Event.decrypted", this.onEventDecrypted);
|
||||
cli.on("accountData", this.onAccountData);
|
||||
cli.on("Group.myMembership", this._onGroupMyMembership);
|
||||
|
||||
this.refreshRoomList();
|
||||
|
||||
|
@ -155,7 +156,9 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
}
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._delayedRefreshRoomList.cancelPendingCall();
|
||||
|
@ -224,12 +227,21 @@ module.exports = React.createClass({
|
|||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onEventDecrypted: function(ev) {
|
||||
// An event being decrypted may mean we need to re-order the room list
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onAccountData: function(ev) {
|
||||
if (ev.getType() == 'm.direct') {
|
||||
this._delayedRefreshRoomList();
|
||||
}
|
||||
},
|
||||
|
||||
_onGroupMyMembership: function(group) {
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
_delayedRefreshRoomList: new rate_limited_func(function() {
|
||||
this.refreshRoomList();
|
||||
}, 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() {
|
||||
var RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||
const RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||
|
||||
const inviteSectionExtraTiles = this._makeGroupInviteTiles();
|
||||
|
||||
var self = this;
|
||||
return (
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||
|
@ -555,12 +583,15 @@ module.exports = React.createClass({
|
|||
label={ _t('Invites') }
|
||||
editable={ false }
|
||||
order="recent"
|
||||
isInvite={true}
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
onShowMoreRooms={ self.onShowMoreRooms }
|
||||
extraTiles={ inviteSectionExtraTiles }
|
||||
/>
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||
label={ _t('Favourites') }
|
||||
|
|
|
@ -24,8 +24,6 @@ import sdk from '../../../index';
|
|||
import Modal from '../../../Modal';
|
||||
import ObjectUtils from '../../../ObjectUtils';
|
||||
import dis from '../../../dispatcher';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
|
@ -92,7 +90,6 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
onSaveClick: React.PropTypes.func,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -118,14 +115,10 @@ module.exports = React.createClass({
|
|||
// Default to false if it's undefined, otherwise react complains about changing
|
||||
// components from uncontrolled to controlled
|
||||
isRoomPublished: this._originalIsRoomPublished || false,
|
||||
scalar_error: null,
|
||||
showIntegrationsError: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
ScalarMessaging.startListening();
|
||||
|
||||
MatrixClientPeg.get().on("RoomMember.membership", this._onRoomMemberMembership);
|
||||
|
||||
MatrixClientPeg.get().getRoomDirectoryVisibility(
|
||||
|
@ -137,18 +130,6 @@ module.exports = React.createClass({
|
|||
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({
|
||||
action: 'ui_opacity',
|
||||
sideOpacity: 0.3,
|
||||
|
@ -157,8 +138,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
ScalarMessaging.stopListening();
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomMember.membership", this._onRoomMemberMembership);
|
||||
|
@ -308,6 +287,9 @@ module.exports = React.createClass({
|
|||
promises.push(ps);
|
||||
}
|
||||
|
||||
// related groups
|
||||
promises.push(this.saveRelatedGroups());
|
||||
|
||||
// encryption
|
||||
p = this.saveEnableEncryption();
|
||||
if (!p.isFulfilled()) {
|
||||
|
@ -325,6 +307,11 @@ module.exports = React.createClass({
|
|||
return this.refs.alias_settings.saveSettings();
|
||||
},
|
||||
|
||||
saveRelatedGroups: function() {
|
||||
if (!this.refs.related_groups) { return Promise.resolve(); }
|
||||
return this.refs.related_groups.saveSettings();
|
||||
},
|
||||
|
||||
saveColor: function() {
|
||||
if (!this.refs.color_settings) { return Promise.resolve(); }
|
||||
return this.refs.color_settings.saveSettings();
|
||||
|
@ -514,28 +501,6 @@ module.exports = React.createClass({
|
|||
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() {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
|
@ -634,6 +599,7 @@ module.exports = React.createClass({
|
|||
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
||||
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
|
||||
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||
var RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
|
@ -666,6 +632,14 @@ module.exports = React.createClass({
|
|||
|
||||
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;
|
||||
if (Object.keys(user_levels).length) {
|
||||
userLevelsSection =
|
||||
|
@ -797,46 +771,10 @@ module.exports = React.createClass({
|
|||
</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 (
|
||||
<div className="mx_RoomSettings">
|
||||
|
||||
{ leaveButton }
|
||||
{ integrationsButton }
|
||||
|
||||
{ tagsSection }
|
||||
|
||||
|
@ -872,7 +810,7 @@ module.exports = React.createClass({
|
|||
<input type="checkbox" disabled={ !roomState.mayClientSendStateEvent("m.room.aliases", cli) }
|
||||
onChange={ this._onToggle.bind(this, "isRoomPublished", true, false)}
|
||||
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>
|
||||
</div>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
|
@ -926,6 +864,8 @@ module.exports = React.createClass({
|
|||
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
|
||||
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
|
||||
|
||||
{ relatedGroupsSection }
|
||||
|
||||
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
|
||||
|
||||
<h3>{ _t('Permissions') }</h3>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -27,6 +28,8 @@ var RoomNotifs = require('../../../RoomNotifs');
|
|||
var FormattingUtils = require('../../../utils/FormattingUtils');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTile',
|
||||
|
@ -39,7 +42,6 @@ module.exports = React.createClass({
|
|||
|
||||
room: React.PropTypes.object.isRequired,
|
||||
collapsed: React.PropTypes.bool.isRequired,
|
||||
selected: React.PropTypes.bool.isRequired,
|
||||
unread: React.PropTypes.bool.isRequired,
|
||||
highlight: React.PropTypes.bool.isRequired,
|
||||
isInvite: React.PropTypes.bool.isRequired,
|
||||
|
@ -58,6 +60,7 @@ module.exports = React.createClass({
|
|||
badgeHover : false,
|
||||
menuDisplayed: false,
|
||||
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() {
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -96,6 +106,7 @@ module.exports = React.createClass({
|
|||
if (cli) {
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||
},
|
||||
|
||||
onClick: function(ev) {
|
||||
|
@ -174,7 +185,7 @@ module.exports = React.createClass({
|
|||
|
||||
var classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
'mx_RoomTile_selected': this.props.selected,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
'mx_RoomTile_unread': this.props.unread,
|
||||
'mx_RoomTile_unreadNotify': notifBadges,
|
||||
'mx_RoomTile_highlight': mentionBadges,
|
||||
|
@ -221,7 +232,7 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
if (this.props.selected) {
|
||||
if (this.state.selected) {
|
||||
let nameSelected = <EmojiText>{name}</EmojiText>;
|
||||
|
||||
label = <div title={ name } className={ nameClasses } dir="auto">{ nameSelected }</div>;
|
||||
|
|
|
@ -26,7 +26,7 @@ export function CancelButton(props) {
|
|||
return (
|
||||
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
|
||||
<img src="img/cancel.svg" className='mx_filterFlipColor'
|
||||
width="18" height="18" alt={_t("Cancel")}/>
|
||||
width="18" height="18" alt={_t("Cancel")} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -208,7 +208,7 @@ module.exports = React.createClass({
|
|||
if (!this.state.cachedPassword) {
|
||||
currentPassword = <div className={rowClassName}>
|
||||
<div className={rowLabelClassName}>
|
||||
<label htmlFor="passwordold">Current password</label>
|
||||
<label htmlFor="passwordold">{ _t('Current password') }</label>
|
||||
</div>
|
||||
<div className={rowInputClassName}>
|
||||
<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
|
||||
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
Modal.createTrackedDialog('Delete Device Dialog', InteractiveAuthDialog, {
|
||||
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
|
||||
title: _t("Authentication"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
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…
Reference in a new issue