Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into set_default_federate_by_settings

This commit is contained in:
Michael Telatynski 2017-10-04 22:35:29 +01:00
commit 7492f2dffa
No known key found for this signature in database
GPG key ID: 0435A1D4BBD34D64
149 changed files with 7702 additions and 5253 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -107,6 +107,9 @@ export default class BasePlatform {
isElectron(): boolean { return false; }
setupScreenSharingForIframe() {
}
/**
* Restarts the application, without neccessarily reloading
* any application code

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = [];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -157,7 +157,7 @@ module.exports = React.createClass({
},
componentDidMount: function() {
this.checkFillState();
this.checkScroll();
},
componentDidUpdate: function() {

View file

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

View file

@ -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()}>
&lt;{ _t("click to reveal") }&gt;
</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>

View file

@ -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&#39;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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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}>&#946;</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>
);
},

View file

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

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

View file

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

View file

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

View 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,
};

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

View 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,
};

View file

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

View file

@ -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>&nbsp;
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
<div className="mx_Spinner_Msg">{ msg }</div>&nbsp;
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
</div>
);
},

View file

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

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -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>
);
}
},
});

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

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

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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