Merge branches 'develop' and 'kegan/hide-join-part' of github.com:matrix-org/matrix-react-sdk into kegan/hide-join-part

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

# Conflicts:
#	src/components/structures/UserSettings.js
pull/21833/head
Michael Telatynski 2017-06-12 13:22:52 +01:00
commit c1cbdbcc4f
155 changed files with 8785 additions and 2094 deletions

185
.eslintignore.errorfiles Normal file
View File

@ -0,0 +1,185 @@
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
src/AddThreepid.js
src/async-components/views/dialogs/EncryptedEventDialog.js
src/autocomplete/AutocompleteProvider.js
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/Avatar.js
src/BasePlatform.js
src/CallHandler.js
src/component-index.js
src/components/structures/ContextualMenu.js
src/components/structures/CreateRoom.js
src/components/structures/FilePanel.js
src/components/structures/InteractiveAuth.js
src/components/structures/LoggedInView.js
src/components/structures/login/ForgotPassword.js
src/components/structures/login/Login.js
src/components/structures/login/PostRegistration.js
src/components/structures/login/Registration.js
src/components/structures/MessagePanel.js
src/components/structures/NotificationPanel.js
src/components/structures/RoomStatusBar.js
src/components/structures/RoomView.js
src/components/structures/ScrollPanel.js
src/components/structures/TimelinePanel.js
src/components/structures/UploadBar.js
src/components/views/avatars/BaseAvatar.js
src/components/views/avatars/MemberAvatar.js
src/components/views/avatars/RoomAvatar.js
src/components/views/create_room/CreateRoomButton.js
src/components/views/create_room/Presets.js
src/components/views/create_room/RoomAlias.js
src/components/views/dialogs/ChatCreateOrReuseDialog.js
src/components/views/dialogs/ChatInviteDialog.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
src/components/views/elements/AddressSelector.js
src/components/views/elements/AddressTile.js
src/components/views/elements/CreateRoomButton.js
src/components/views/elements/DeviceVerifyButtons.js
src/components/views/elements/DirectorySearchBox.js
src/components/views/elements/Dropdown.js
src/components/views/elements/EditableText.js
src/components/views/elements/EditableTextContainer.js
src/components/views/elements/HomeButton.js
src/components/views/elements/LanguageDropdown.js
src/components/views/elements/MemberEventListSummary.js
src/components/views/elements/PowerSelector.js
src/components/views/elements/ProgressBar.js
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
src/components/views/login/CountryDropdown.js
src/components/views/login/CustomServerDialog.js
src/components/views/login/InteractiveAuthEntryComponents.js
src/components/views/login/LoginHeader.js
src/components/views/login/PasswordLogin.js
src/components/views/login/RegistrationForm.js
src/components/views/login/ServerConfig.js
src/components/views/messages/MAudioBody.js
src/components/views/messages/MessageEvent.js
src/components/views/messages/MFileBody.js
src/components/views/messages/MImageBody.js
src/components/views/messages/MVideoBody.js
src/components/views/messages/RoomAvatarEvent.js
src/components/views/messages/TextualBody.js
src/components/views/messages/TextualEvent.js
src/components/views/room_settings/AliasSettings.js
src/components/views/room_settings/ColorSettings.js
src/components/views/room_settings/UrlPreviewSettings.js
src/components/views/rooms/Autocomplete.js
src/components/views/rooms/AuxPanel.js
src/components/views/rooms/EntityTile.js
src/components/views/rooms/EventTile.js
src/components/views/rooms/LinkPreviewWidget.js
src/components/views/rooms/MemberDeviceInfo.js
src/components/views/rooms/MemberInfo.js
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/RoomHeader.js
src/components/views/rooms/RoomList.js
src/components/views/rooms/RoomNameEditor.js
src/components/views/rooms/RoomPreviewBar.js
src/components/views/rooms/RoomSettings.js
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
src/components/views/settings/ChangeAvatar.js
src/components/views/settings/ChangeDisplayName.js
src/components/views/settings/ChangePassword.js
src/components/views/settings/DevicesPanel.js
src/components/views/settings/DevicesPanelEntry.js
src/components/views/settings/EnableNotificationsButton.js
src/components/views/voip/CallView.js
src/components/views/voip/IncomingCallBox.js
src/components/views/voip/VideoFeed.js
src/components/views/voip/VideoView.js
src/ContentMessages.js
src/createRoom.js
src/DateUtils.js
src/email.js
src/Entities.js
src/extend.js
src/HtmlUtils.js
src/ImageUtils.js
src/Invite.js
src/languageHandler.js
src/linkify-matrix.js
src/Login.js
src/Markdown.js
src/MatrixClientPeg.js
src/Modal.js
src/Notifier.js
src/ObjectUtils.js
src/PasswordReset.js
src/PlatformPeg.js
src/Presence.js
src/ratelimitedfunc.js
src/Resend.js
src/RichText.js
src/Roles.js
src/RoomListSorter.js
src/RoomNotifs.js
src/Rooms.js
src/RtsClient.js
src/ScalarAuthClient.js
src/ScalarMessaging.js
src/SdkConfig.js
src/Skinner.js
src/SlashCommands.js
src/stores/LifecycleStore.js
src/TabComplete.js
src/TabCompleteEntries.js
src/TextForEvent.js
src/Tinter.js
src/UiEffects.js
src/Unread.js
src/UserActivity.js
src/utils/DecryptFile.js
src/utils/DMRoomMap.js
src/utils/FormattingUtils.js
src/utils/MultiInviter.js
src/utils/Receipt.js
src/Velociraptor.js
src/VelocityBounce.js
src/WhoIsTyping.js
src/wrappers/WithMatrixClient.js
test/all-tests.js
test/components/structures/login/Registration-test.js
test/components/structures/MessagePanel-test.js
test/components/structures/ScrollPanel-test.js
test/components/structures/TimelinePanel-test.js
test/components/stub-component.js
test/components/views/dialogs/InteractiveAuthDialog-test.js
test/components/views/elements/MemberEventListSummary-test.js
test/components/views/login/RegistrationForm-test.js
test/components/views/rooms/MessageComposerInput-test.js
test/mock-clock.js
test/skinned-sdk.js
test/stores/RoomViewStore-test.js
test/test-utils.js

2
.gitignore vendored
View File

@ -12,3 +12,5 @@ npm-debug.log
/.idea
/src/component-index.js
.DS_Store

View File

@ -5,5 +5,4 @@ install:
- npm install
- (cd node_modules/matrix-js-sdk && npm install)
script:
- npm run test
- ./.travis-test-riot.sh
./scripts/travis.sh

View File

@ -1,3 +1,185 @@
Changes in [0.9.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.2) (2017-06-06)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.1...v0.9.2)
* Hotfix: Allow password reset when logged in
[\#1044](https://github.com/matrix-org/matrix-react-sdk/pull/1044)
Changes in [0.9.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.1) (2017-06-02)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.0...v0.9.1)
* Update from Weblate.
[\#1012](https://github.com/matrix-org/matrix-react-sdk/pull/1012)
* typo, missing import and mis-casing
[\#1014](https://github.com/matrix-org/matrix-react-sdk/pull/1014)
* Update from Weblate.
[\#1010](https://github.com/matrix-org/matrix-react-sdk/pull/1010)
Changes in [0.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.0) (2017-06-02)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.0-rc.2...v0.9.0)
* sync pt with pt_BR
[\#1009](https://github.com/matrix-org/matrix-react-sdk/pull/1009)
* Update from Weblate.
[\#1008](https://github.com/matrix-org/matrix-react-sdk/pull/1008)
* Update from Weblate.
[\#1003](https://github.com/matrix-org/matrix-react-sdk/pull/1003)
* allow hiding redactions, restoring old behaviour
[\#1004](https://github.com/matrix-org/matrix-react-sdk/pull/1004)
* Add missing translations
[\#1005](https://github.com/matrix-org/matrix-react-sdk/pull/1005)
Changes in [0.9.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.0-rc.2) (2017-06-02)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.0-rc.1...v0.9.0-rc.2)
* Update from Weblate.
[\#1002](https://github.com/matrix-org/matrix-react-sdk/pull/1002)
* webrtc config electron
[\#850](https://github.com/matrix-org/matrix-react-sdk/pull/850)
* enable useCompactLayout user setting an add a class when it's enabled
[\#986](https://github.com/matrix-org/matrix-react-sdk/pull/986)
* Update from Weblate.
[\#987](https://github.com/matrix-org/matrix-react-sdk/pull/987)
* Translation fixes for everything but src/components
[\#990](https://github.com/matrix-org/matrix-react-sdk/pull/990)
* Fix tests
[\#1001](https://github.com/matrix-org/matrix-react-sdk/pull/1001)
* Fix tests for PR #989
[\#999](https://github.com/matrix-org/matrix-react-sdk/pull/999)
* Revert "Revert "add labels to language picker""
[\#1000](https://github.com/matrix-org/matrix-react-sdk/pull/1000)
* maybe fixxy [Electron] external thing?
[\#997](https://github.com/matrix-org/matrix-react-sdk/pull/997)
* travisci: Don't run the riot-web tests if the react-sdk tests fail
[\#992](https://github.com/matrix-org/matrix-react-sdk/pull/992)
* Support 12hr time on DateSeparator
[\#991](https://github.com/matrix-org/matrix-react-sdk/pull/991)
* Revert "add labels to language picker"
[\#994](https://github.com/matrix-org/matrix-react-sdk/pull/994)
* Call MatrixClient.clearStores on logout
[\#983](https://github.com/matrix-org/matrix-react-sdk/pull/983)
* Matthew/room avatar event
[\#988](https://github.com/matrix-org/matrix-react-sdk/pull/988)
* add labels to language picker
[\#989](https://github.com/matrix-org/matrix-react-sdk/pull/989)
* Update from Weblate.
[\#981](https://github.com/matrix-org/matrix-react-sdk/pull/981)
Changes in [0.9.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.0-rc.1) (2017-06-01)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.9...v0.9.0-rc.1)
* Fix rare case where presence duration is undefined
[\#982](https://github.com/matrix-org/matrix-react-sdk/pull/982)
* add concept of platform handling loudNotifications (bings/pings/whatHaveYou)
[\#985](https://github.com/matrix-org/matrix-react-sdk/pull/985)
* Fixes to i18n code
[\#984](https://github.com/matrix-org/matrix-react-sdk/pull/984)
* Update from Weblate.
[\#978](https://github.com/matrix-org/matrix-react-sdk/pull/978)
* Add partial support for RTL languages
[\#955](https://github.com/matrix-org/matrix-react-sdk/pull/955)
* Added two strings to translate
[\#975](https://github.com/matrix-org/matrix-react-sdk/pull/975)
* Update from Weblate.
[\#976](https://github.com/matrix-org/matrix-react-sdk/pull/976)
* Update from Weblate.
[\#974](https://github.com/matrix-org/matrix-react-sdk/pull/974)
* Initial Electron Settings - for Auto Launch
[\#920](https://github.com/matrix-org/matrix-react-sdk/pull/920)
* Fix missing string in the room settings
[\#973](https://github.com/matrix-org/matrix-react-sdk/pull/973)
* fix error in i18n string
[\#972](https://github.com/matrix-org/matrix-react-sdk/pull/972)
* Update from Weblate.
[\#970](https://github.com/matrix-org/matrix-react-sdk/pull/970)
* Support 12hr time in full date
[\#971](https://github.com/matrix-org/matrix-react-sdk/pull/971)
* Add _tJsx()
[\#968](https://github.com/matrix-org/matrix-react-sdk/pull/968)
* Update from Weblate.
[\#966](https://github.com/matrix-org/matrix-react-sdk/pull/966)
* Remove space between time and AM/PM
[\#969](https://github.com/matrix-org/matrix-react-sdk/pull/969)
* Piwik Analytics
[\#948](https://github.com/matrix-org/matrix-react-sdk/pull/948)
* Update from Weblate.
[\#965](https://github.com/matrix-org/matrix-react-sdk/pull/965)
* Improve ChatInviteDialog perf by ditching fuse, using indexOf and
lastActiveTs()
[\#960](https://github.com/matrix-org/matrix-react-sdk/pull/960)
* Say "X removed the room name" instead of showing nothing
[\#958](https://github.com/matrix-org/matrix-react-sdk/pull/958)
* roomview/roomheader fixes
[\#959](https://github.com/matrix-org/matrix-react-sdk/pull/959)
* Update from Weblate.
[\#953](https://github.com/matrix-org/matrix-react-sdk/pull/953)
* fix i18n in a situation where navigator.languages=[]
[\#956](https://github.com/matrix-org/matrix-react-sdk/pull/956)
* `t_` -> `_t` fix typo
[\#957](https://github.com/matrix-org/matrix-react-sdk/pull/957)
* Change redact -> remove for clarity
[\#831](https://github.com/matrix-org/matrix-react-sdk/pull/831)
* Update from Weblate.
[\#950](https://github.com/matrix-org/matrix-react-sdk/pull/950)
* fix mis-linting - missed it in code review :(
[\#952](https://github.com/matrix-org/matrix-react-sdk/pull/952)
* i18n fixes
[\#951](https://github.com/matrix-org/matrix-react-sdk/pull/951)
* Message Forwarding
[\#812](https://github.com/matrix-org/matrix-react-sdk/pull/812)
* don't focus_composer on window focus
[\#944](https://github.com/matrix-org/matrix-react-sdk/pull/944)
* Fix vector-im/riot-web#4042
[\#947](https://github.com/matrix-org/matrix-react-sdk/pull/947)
* import _t, drop two unused imports
[\#946](https://github.com/matrix-org/matrix-react-sdk/pull/946)
* Fix punctuation in TextForEvent to be i18n'd consistently
[\#945](https://github.com/matrix-org/matrix-react-sdk/pull/945)
* actually wire up alwaysShowTimestamps
[\#940](https://github.com/matrix-org/matrix-react-sdk/pull/940)
* Update from Weblate.
[\#943](https://github.com/matrix-org/matrix-react-sdk/pull/943)
* Update from Weblate.
[\#942](https://github.com/matrix-org/matrix-react-sdk/pull/942)
* Update from Weblate.
[\#941](https://github.com/matrix-org/matrix-react-sdk/pull/941)
* Update from Weblate.
[\#938](https://github.com/matrix-org/matrix-react-sdk/pull/938)
* Fix PM being AM
[\#939](https://github.com/matrix-org/matrix-react-sdk/pull/939)
* pass call state through dispatcher, for poor electron
[\#918](https://github.com/matrix-org/matrix-react-sdk/pull/918)
* Translations!
[\#934](https://github.com/matrix-org/matrix-react-sdk/pull/934)
* Remove suffix and prefix from login input username
[\#906](https://github.com/matrix-org/matrix-react-sdk/pull/906)
* Kierangould/12hourtimestamp
[\#903](https://github.com/matrix-org/matrix-react-sdk/pull/903)
* Don't include src in the test resolve root
[\#931](https://github.com/matrix-org/matrix-react-sdk/pull/931)
* Make the linked versions open a new tab, turt2live complained :P
[\#910](https://github.com/matrix-org/matrix-react-sdk/pull/910)
* Fix lint errors in SlashCommands
[\#919](https://github.com/matrix-org/matrix-react-sdk/pull/919)
* autoFocus input box
[\#911](https://github.com/matrix-org/matrix-react-sdk/pull/911)
* Make travis test against riot-web new-guest-access
[\#917](https://github.com/matrix-org/matrix-react-sdk/pull/917)
* Add right-branch logic to travis test script
[\#916](https://github.com/matrix-org/matrix-react-sdk/pull/916)
* Group e2e keys into blocks of 4 characters
[\#914](https://github.com/matrix-org/matrix-react-sdk/pull/914)
* Factor out DeviceVerifyDialog
[\#913](https://github.com/matrix-org/matrix-react-sdk/pull/913)
* Fix 'missing page_type' error
[\#909](https://github.com/matrix-org/matrix-react-sdk/pull/909)
* code style update
[\#904](https://github.com/matrix-org/matrix-react-sdk/pull/904)
Changes in [0.8.9](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9) (2017-05-22)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.9-rc.1...v0.8.9)

View File

@ -21,6 +21,11 @@ npm run test
# run eslint
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
# delete the old tarball, if it exists
rm -f matrix-react-sdk-*.tgz

View File

@ -177,6 +177,11 @@ module.exports = function (config) {
],
},
devtool: 'inline-source-map',
externals: {
// Don't try to bundle electron: leave it as a commonjs dependency
// (the 'commonjs' here means it will output a 'require')
"electron": "commonjs electron",
},
},
webpackMiddleware: {

View File

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.8.9",
"version": "0.9.2",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -64,8 +64,9 @@
"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.7.10",
"optimist": "^0.6.1",
"prop-types": "^15.5.8",
"q": "^1.4.1",
"react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2",

View File

@ -178,12 +178,12 @@ sub read_src_strings {
$src =~ s/"\s*\+\s*"//g;
$file =~ s/^.*\/src/src/;
while ($src =~ /_t\(\s*'(.*?[^\\])'/sg) {
while ($src =~ /_t(?:Jsx)?\(\s*'(.*?[^\\])'/sg) {
my $s = $1;
$s =~ s/\\'/'/g;
push @$strings, [$s, $file];
}
while ($src =~ /_t\(\s*"(.*?[^\\])"/sg) {
while ($src =~ /_t(?:Jsx)?\(\s*"(.*?[^\\])"/sg) {
push @$strings, [$1, $file];
}
}

View File

@ -48,6 +48,29 @@ Guests can't use labs features. Please register.
A new password must be entered.
Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.
Guests cannot join this room even if explicitly invited.
Guest users can't invite users. Please register to invite.
This room is inaccessible to guests. You may be able to join if you register.
delete the alias.
remove %(name)s from the directory.
Conference call failed.
Conference calling is in development and may not be reliable.
Guest users can't create new rooms. Please register to create room and start a chat.
Server may be unavailable, overloaded, or you hit a bug.
Server unavailable, overloaded, or something else went wrong.
You are already in a call.
You cannot place VoIP calls in this browser.
You cannot place a call with yourself.
Your email address does not appear to be associated with a Matrix ID on this Homeserver.
Guest users can't upload files. Please register to upload.
Some of your messages have not been sent.
This room is private or inaccessible to guests. You may be able to join if you register.
Tried to load a specific point in this room's timeline, but was unable to find it.
Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.
This action cannot be performed by a guest user. Please register to be able to do this.
Tried to load a specific point in this room's timeline, but was unable to find it.
Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.
You are trying to access %(roomName)s.
You will not be able to undo this change as you are promoting the user to have the same power level as yourself.
EOT
)];
}
@ -71,7 +94,7 @@ if ($_ =~ m/^(\s+)"(.*?)"(: *)"(.*?)"(,?)$/) {
$sub = 1;
}
if ($src eq $fixup && $dst !~ /\.$/) {
if ($ARGV !~ /(zh_Hans|zh_Hant|th)\.json$/ && $src eq $fixup && $dst !~ /\.$/) {
print STDERR "fixing up dst: $dst\n";
$dst .= '.';
$sub = 1;

View File

@ -0,0 +1,21 @@
#!/bin/sh
#
# generates .eslintignore.errorfiles to list the files which have errors in,
# so that they can be ignored in future automated linting.
out=.eslintignore.errorfiles
cd `dirname $0`/..
echo "generating $out"
{
cat <<EOF
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
EOF
./node_modules/.bin/eslint --no-ignore -f json src test |
jq -r '.[] | select((.errorCount + .warningCount) > 0) | .filePath' |
sed -e 's/.*matrix-react-sdk\///';
} > "$out"

11
scripts/travis.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
set -ex
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

View File

@ -19,8 +19,10 @@ import MatrixClientPeg from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig';
function redact(str) {
return str.replace(/#\/(room|user)\/(.+)/, "#/$1/<redacted>");
function getRedactedUrl() {
const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/<redacted>");
// hardcoded url to make piwik happy
return 'https://riot.im/app/' + redactedHash;
}
const customVariables = {
@ -28,6 +30,7 @@ const customVariables = {
'App Version': 2,
'User Type': 3,
'Chosen Language': 4,
'Instance': 5,
};
@ -53,6 +56,7 @@ class Analytics {
* but this is second best, Piwik should not pull anything implicitly.
*/
disable() {
this.trackEvent('Analytics', 'opt-out');
this.disabled = true;
}
@ -84,6 +88,10 @@ class Analytics {
this._setVisitVariable('Chosen Language', getCurrentLanguage());
if (window.location.hostname === 'riot.im') {
this._setVisitVariable('Instance', window.location.pathname);
}
(function() {
const g = document.createElement('script');
const s = document.getElementsByTagName('script')[0];
@ -108,7 +116,7 @@ class Analytics {
this.firstPage = false;
return;
}
this._paq.push(['setCustomUrl', redact(window.location.href)]);
this._paq.push(['setCustomUrl', getRedactedUrl()]);
this._paq.push(['trackPageView']);
}

View File

@ -71,6 +71,9 @@ export default class BasePlatform {
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {
}
loudNotification(ev: Event, room: Object) {
}
/**
* Returns a promise that resolves to a string representing
* the current version of the application.

View File

@ -226,7 +226,7 @@ function _onAction(payload) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t('Existing Call'),
description: _t('You are already in a call') + '.',
description: _t('You are already in a call.'),
});
return; // don't allow >1 call to be placed.
}
@ -236,7 +236,7 @@ function _onAction(payload) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t('VoIP is unsupported'),
description: _t('You cannot place VoIP calls in this browser') + '.',
description: _t('You cannot place VoIP calls in this browser.'),
});
return;
}
@ -251,7 +251,7 @@ function _onAction(payload) {
if (members.length <= 1) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
description: _t('You cannot place a call with yourself') + '.',
description: _t('You cannot place a call with yourself.'),
});
return;
}
@ -284,7 +284,7 @@ function _onAction(payload) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t('VoIP is unsupported'),
description: _t('You cannot place VoIP calls in this browser') + '.',
description: _t('You cannot place VoIP calls in this browser.'),
});
}
else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
@ -303,7 +303,7 @@ function _onAction(payload) {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: _t('Warning!'),
description: _t('Conference calling is in development and may not be reliable') + '.',
description: _t('Conference calling is in development and may not be reliable.'),
onFinished: confirm=>{
if (confirm) {
ConferenceHandler.createNewMatrixCall(
@ -315,7 +315,7 @@ function _onAction(payload) {
console.error("Conference call failed: " + err);
Modal.createDialog(ErrorDialog, {
title: _t('Failed to set up conference call'),
description: _t('Conference call failed') + '. ' + ((err && err.message) ? err.message : ''),
description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
});
});
}

64
src/CallMediaHandler.js Normal file
View File

@ -0,0 +1,64 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
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 UserSettingsStore from './UserSettingsStore';
import * as Matrix from 'matrix-js-sdk';
export default {
getDevices: function() {
// Only needed for Electron atm, though should work in modern browsers
// once permission has been granted to the webapp
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
const audioIn = [];
const videoIn = [];
if (devices.some((device) => !device.label)) return false;
devices.forEach((device) => {
switch (device.kind) {
case 'audioinput': audioIn.push(device); break;
case 'videoinput': videoIn.push(device); break;
}
});
// console.log("Loaded WebRTC Devices", mediaDevices);
return {
audioinput: audioIn,
videoinput: videoIn,
};
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
},
loadDevices: function() {
// this.getDevices().then((devices) => {
const localSettings = UserSettingsStore.getLocalSettings();
// // if deviceId is not found, automatic fallback is in spec
// // recall previously stored inputs if any
Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']);
Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']);
// });
},
setAudioInput: function(deviceId) {
UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId);
Matrix.setMatrixCallAudioInput(deviceId);
},
setVideoInput: function(deviceId) {
UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId);
Matrix.setMatrixCallVideoInput(deviceId);
},
};

View File

@ -345,6 +345,7 @@ export function bodyToHtml(content, highlights, opts) {
}
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
safeBody = unicodeToImage(safeBody);
safeBody = addCodeCopyButton(safeBody);
}
finally {
delete sanitizeHtmlParams.textFilter;
@ -360,7 +361,24 @@ export function bodyToHtml(content, highlights, opts) {
'mx_EventTile_bigEmoji': emojiBody,
'markdown-body': isHtml,
});
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} />;
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) {

View File

@ -32,4 +32,5 @@ module.exports = {
DELETE: 46,
KEY_D: 68,
KEY_E: 69,
KEY_M: 77,
};

View File

@ -187,6 +187,14 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
// returns a promise which resolves to true if a session is found in
// localstorage
//
// N.B. Lifecycle.js should not maintain any further localStorage state, we
// are moving towards using SessionStore to keep track of state related
// to the current session (which is typically backed by localStorage).
//
// The plan is to gradually move the localStorage access done here into
// SessionStore to avoid bugs where the view becomes out-of-sync with
// localStorage (e.g. teamToken, isGuest etc.)
function _restoreFromLocalStorage() {
if (!localStorage) {
return q(false);
@ -232,7 +240,9 @@ function _handleRestoreFailure(e) {
let msg = e.message;
if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") {
msg = _t(
'You need to log back in to generate end-to-end encryption keys for this device and submit the public key to your homeserver. This is a once off; sorry for the inconvenience.'
'You need to log back in to generate end-to-end encryption keys'
+ ' for this device and submit the public key to your homeserver.'
+ ' This is a once off; sorry for the inconvenience.',
);
_clearLocalStorage();
@ -312,6 +322,16 @@ export function setLoggedIn(credentials) {
localStorage.setItem("mx_device_id", credentials.deviceId);
}
// The user registered as a PWLU (PassWord-Less User), the generated password
// is cached here such that the user can change it at a later time.
if (credentials.password) {
// Update SessionStore
dis.dispatch({
action: 'cached_password',
cachedPassword: credentials.password,
});
}
console.log("Session persisted for %s", credentials.userId);
} catch (e) {
console.warn("Error using local storage: can't persist session!", e);

View File

@ -16,6 +16,7 @@ limitations under the License.
*/
import Matrix from "matrix-js-sdk";
import { _t } from "./languageHandler";
import q from 'q';
import url from 'url';
@ -96,11 +97,6 @@ export default class Login {
guest: true
};
}, (error) => {
if (error.httpStatus === 403) {
error.friendlyText = "Guest access is disabled on this Home Server.";
} else {
error.friendlyText = "Failed to register as guest: " + error.data;
}
throw error;
});
}
@ -156,15 +152,7 @@ export default class Login {
accessToken: data.access_token
});
}, function(error) {
if (error.httpStatus == 400 && loginParams.medium) {
error.friendlyText = (
'This Home Server does not support login using email address.'
);
}
else if (error.httpStatus === 403) {
error.friendlyText = (
'Incorrect username and/or password.'
);
if (error.httpStatus === 403) {
if (self._fallbackHsUrl) {
var fbClient = Matrix.createClient({
baseUrl: self._fallbackHsUrl,
@ -185,11 +173,6 @@ export default class Login {
});
}
}
else {
error.friendlyText = (
'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
);
}
throw error;
});
}

View File

@ -16,7 +16,6 @@ limitations under the License.
'use strict';
import q from "q";
import Matrix from 'matrix-js-sdk';
import utils from 'matrix-js-sdk/lib/utils';
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';

View File

@ -250,6 +250,7 @@ const Notifier = {
this._displayPopupNotification(ev, room);
}
if (actions.tweaks.sound && this.isAudioEnabled()) {
PlatformPeg.get().loudNotification(ev, room);
this._playAudioNotification(ev, room);
}
}

View File

@ -82,7 +82,7 @@ class PasswordReset {
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
}
else if (err.httpStatus === 404) {
err.message = _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver') + '.';
err.message = _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
}
else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`;

View File

@ -15,7 +15,6 @@ limitations under the License.
*/
import MatrixClientPeg from './MatrixClientPeg';
import DMRoomMap from './utils/DMRoomMap';
import q from 'q';
/**

View File

@ -94,6 +94,22 @@ Example:
}
}
get_membership_count
--------------------
Get the number of joined users in the room.
Request:
- room_id is the room to get the count in.
Response:
78
Example:
{
action: "get_membership_count",
room_id: "!foo:bar",
response: 78
}
membership_state AND bot_options
--------------------------------
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.
@ -256,6 +272,21 @@ function botOptions(event, roomId, userId) {
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
}
function getMembershipCount(event, roomId) {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
return;
}
const room = client.getRoom(roomId);
if (!room) {
sendError(event, _t('This room is not recognised.'));
return;
}
const count = room.getJoinedMembers().length;
sendResponse(event, count);
}
function returnStateEvent(event, roomId, eventType, stateKey) {
const client = MatrixClientPeg.get();
if (!client) {
@ -343,6 +374,9 @@ const onMessage = function(event) {
} else if (event.data.action === "set_plumbing_state") {
setPlumbingState(event, roomId, event.data.status);
return;
} else if (event.data.action === "get_membership_count") {
getMembershipCount(event, roomId);
return;
}
if (!userId) {

View File

@ -23,22 +23,28 @@ class Skinner {
if (this.components === null) {
throw new Error(
"Attempted to get a component before a skin has been loaded."+
"This is probably because either:"+
" This is probably because either:"+
" a) Your app has not called sdk.loadSkin(), or"+
" b) A component has called getComponent at the root level"
" b) A component has called getComponent at the root level",
);
}
var comp = this.components[name];
if (comp) {
return comp;
}
let comp = this.components[name];
// XXX: Temporarily also try 'views.' as we're currently
// leaving the 'views.' off views.
var comp = this.components['views.'+name];
if (comp) {
return comp;
if (!comp) {
comp = this.components['views.'+name];
}
throw new Error("No such component: "+name);
if (!comp) {
throw new Error("No such component: "+name);
}
// components have to be functions.
const validType = typeof comp === 'function';
if (!validType) {
throw new Error(`Not a valid component: ${name}.`);
}
return comp;
}
load(skinObject) {

View File

@ -186,7 +186,7 @@ const commands = {
if (targetRoomId) { break; }
}
if (!targetRoomId) {
return reject("Unrecognised room alias: " + roomAlias);
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
}
}
}
@ -303,14 +303,14 @@ const commands = {
const device = MatrixClientPeg.get().getStoredDevice(userId, deviceId);
if (!device) {
return reject(`Unknown (user, device) pair: (${userId}, ${deviceId})`);
return reject(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
}
if (device.isVerified()) {
if (device.getFingerprint() === fingerprint) {
return reject(`Device already verified!`);
return reject(_t(`Device already verified!`));
} else {
return reject(`WARNING: Device already verified, but keys do NOT MATCH!`);
return reject(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
}
}
@ -322,12 +322,15 @@ const commands = {
// Tell the user we verified everything!
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Verified key",
title: _t("Verified key"),
description: (
<div>
<p>
The signing key you provided matches the signing key you received
from { userId }'s device { deviceId }. Device marked as verified.
{
_t("The signing key you provided matches the signing key you received " +
"from %(userId)s's device %(deviceId)s. Device marked as verified.",
{userId: userId, deviceId: deviceId})
}
</p>
</div>
),
@ -336,9 +339,13 @@ const commands = {
return success();
} else {
return reject(`WARNING: KEY VERIFICATION FAILED! The signing key for ${userId} and device
${deviceId} is "${device.getFingerprint()}" which does not match the provided key
"${fingerprint}". This could mean your communications are being intercepted!`);
const fprint = device.getFingerprint();
return reject(
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
' "%(fingerprint)s". This could mean your communications are being intercepted!',
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint})
);
}
}
}
@ -383,7 +390,7 @@ module.exports = {
if (commands[cmd]) {
return commands[cmd].run(roomId, args);
} else {
return reject("Unrecognised command: " + input);
return reject(_t("Unrecognised command:") + ' ' + input);
}
}
return null; // not a command

View File

@ -13,9 +13,8 @@ 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 MatrixClientPeg = require("./MatrixClientPeg");
var CallHandler = require("./CallHandler");
import MatrixClientPeg from "./MatrixClientPeg";
import CallHandler from "./CallHandler";
import { _t } from './languageHandler';
import * as Roles from './Roles';
@ -117,7 +116,7 @@ function textForTopicEvent(ev) {
function textForRoomNameEvent(ev) {
var 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});
}
@ -142,9 +141,21 @@ function textForCallAnswerEvent(event) {
}
function textForCallHangupEvent(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 ended the call.', {senderName: senderName}) + ' ' + supported;
const senderName = event.sender ? event.sender.name : _t('Someone');
const eventContent = event.getContent();
let reason = "";
if(!MatrixClientPeg.get().supportsVoip()) {
reason = _t('(not supported by this browser)');
} else if(eventContent.reason) {
if (eventContent.reason === "ice_failed") {
reason = _t('(could not connect media)');
} else if (eventContent.reason === "invite_timeout") {
reason = _t('(no answer)');
} else {
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
}
}
return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason;
}
function textForCallInviteEvent(event) {

View File

@ -17,24 +17,26 @@ limitations under the License.
import q from 'q';
import MatrixClientPeg from './MatrixClientPeg';
import Notifier from './Notifier';
import { _t } from './languageHandler';
/*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
*/
/*
* TODO: Find a way to translate the names of LABS_FEATURES. In other words, guarantee that languages were already loaded before building this array.
*/
export default {
LABS_FEATURES: [
{
name: "New Composer & Autocomplete",
name: "-",
id: 'rich_text_editor',
default: false,
},
],
// horrible but it works. The locality makes this somewhat more palatable.
doTranslations: function() {
this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete");
},
loadProfileInfo: function() {
const cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId);

View File

@ -81,11 +81,13 @@ export default React.createClass({
FileSaver.saveAs(blob, 'riot-keys.txt');
this.props.onFinished(true);
}).catch((e) => {
console.error("Error exporting e2e keys:", e);
if (this._unmounted) {
return;
}
const msg = e.friendlyText || _t('Unknown error');
this.setState({
errStr: e.message,
errStr: msg,
phase: PHASE_EDIT,
});
});
@ -120,7 +122,7 @@ export default React.createClass({
'you have received in encrypted rooms to a local file. You ' +
'will then be able to import the file into another Matrix ' +
'client in the future, so that client will also be able to ' +
'decrypt these messages.'
'decrypt these messages.',
) }
</p>
<p>
@ -130,7 +132,7 @@ export default React.createClass({
'careful to keep it secure. To help with this, you should enter ' +
'a passphrase below, which will be used to encrypt the exported ' +
'data. It will only be possible to import the data by using the ' +
'same passphrase.'
'same passphrase.',
) }
</p>
<div className='error'>
@ -166,11 +168,11 @@ export default React.createClass({
</div>
</div>
<div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value='Export'
<input className='mx_Dialog_primary' type='submit' value={_t('Export')}
disabled={disableForm}
/>
<button onClick={this._onCancelClick} disabled={disableForm}>
Cancel
{_t("Cancel")}
</button>
</div>
</form>

View File

@ -89,11 +89,13 @@ export default React.createClass({
// TODO: it would probably be nice to give some feedback about what we've imported here.
this.props.onFinished(true);
}).catch((e) => {
console.error("Error importing e2e keys:", e);
if (this._unmounted) {
return;
}
const msg = e.friendlyText || _t('Unknown error');
this.setState({
errStr: e.message,
errStr: msg,
phase: PHASE_EDIT,
});
});
@ -122,13 +124,13 @@ export default React.createClass({
'This process allows you to import encryption keys ' +
'that you had previously exported from another Matrix ' +
'client. You will then be able to decrypt any ' +
'messages that the other client could decrypt.'
'messages that the other client could decrypt.',
) }
</p>
<p>
{ _t(
'The export file will be protected with a passphrase. ' +
'You should enter the passphrase here, to decrypt the file.'
'You should enter the passphrase here, to decrypt the file.',
) }
</p>
<div className='error'>
@ -164,11 +166,11 @@ export default React.createClass({
</div>
</div>
<div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value='Import'
<input className='mx_Dialog_primary' type='submit' value={_t('Import')}
disabled={!this.state.enableSubmit || disableForm}
/>
<button onClick={this._onCancelClick} disabled={disableForm}>
Cancel
{_t("Cancel")}
</button>
</div>
</form>

View File

@ -1,3 +1,20 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import type {Completion, SelectionRange} from './Autocompleter';

View File

@ -1,3 +1,19 @@
/*
Copyright 2016 Aviral Dasgupta
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.
*/
// @flow
import type {Component} from 'react';

View File

@ -1,3 +1,20 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';

View File

@ -1,5 +1,20 @@
/*
Copyright 2016 Aviral Dasgupta
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 ReactDOM from 'react-dom';
import classNames from 'classnames';
/* These were earlier stateless functional components but had to be converted

View File

@ -1,4 +1,22 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import 'whatwg-fetch';
@ -75,7 +93,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
}
getName() {
return '🔍 Results from DuckDuckGo';
return '🔍 ' + _t('Results from DuckDuckGo');
}
static getInstance(): DuckDuckGoProvider {

View File

@ -1,3 +1,20 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';

View File

@ -1,3 +1,20 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';

View File

@ -1,7 +1,23 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import Q from 'q';
import Fuse from 'fuse.js';
import {PillCompletion} from './Components';
import sdk from '../index';

View File

@ -17,7 +17,6 @@ limitations under the License.
'use strict';
import React from 'react';
import q from 'q';
import { _t } from '../../languageHandler';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
@ -232,7 +231,7 @@ module.exports = React.createClass({
if (curr_phase == this.phases.ERROR) {
error_box = (
<div className="mx_Error">
{_t('An error occured: %(error_string)s', {error_string: this.state.error_string})}
{_t('An error occurred: %(error_string)s', {error_string: this.state.error_string})}
</div>
);
}
@ -247,7 +246,7 @@ module.exports = React.createClass({
return (
<div className="mx_CreateRoom">
<SimpleRoomHeader title="CreateRoom" collapsedRhs={ this.props.collapsedRhs }/>
<SimpleRoomHeader title={_t("Create Room")} collapsedRhs={ this.props.collapsedRhs }/>
<div className="mx_CreateRoom_body">
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')}/> <br />
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')}/> <br />

View File

@ -19,7 +19,7 @@ import React from 'react';
import Matrix from 'matrix-js-sdk';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
import { _t } from '../../languageHandler';
import { _t, _tJsx } from '../../languageHandler';
/*
* Component which shows the filtered file using a TimelinePanel
@ -91,7 +91,9 @@ var FilePanel = React.createClass({
render: function() {
if (MatrixClientPeg.get().isGuest()) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">You must <a href="#/register">register</a> to use this functionality</div>
<div className="mx_RoomView_empty">
{_tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{sub}</a>)}
</div>
</div>;
} else if (this.noRoom) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">

View File

@ -19,8 +19,6 @@ const InteractiveAuth = Matrix.InteractiveAuth;
import React from 'react';
import sdk from '../../index';
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
export default React.createClass({

View File

@ -18,11 +18,15 @@ limitations under the License.
import * as Matrix from 'matrix-js-sdk';
import React from 'react';
import UserSettingsStore from '../../UserSettingsStore';
import KeyCode from '../../KeyCode';
import Notifier from '../../Notifier';
import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
import sdk from '../../index';
import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore';
import MatrixClientPeg from '../../MatrixClientPeg';
/**
* This is what our MatrixChat shows when we are logged in. The precise view is
@ -39,10 +43,13 @@ export default React.createClass({
propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
page_type: React.PropTypes.string.isRequired,
onRoomIdResolved: React.PropTypes.func,
onRoomCreated: React.PropTypes.func,
onUserSettingsClose: React.PropTypes.func,
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: React.PropTypes.func,
teamToken: React.PropTypes.string,
// and lots and lots of other stuff.
@ -63,6 +70,13 @@ export default React.createClass({
};
},
getInitialState: function() {
return {
// use compact timeline view
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
};
},
componentWillMount: function() {
// stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient;
@ -71,11 +85,35 @@ export default React.createClass({
// RoomView.getScrollState()
this._scrollStateMap = {};
CallMediaHandler.loadDevices();
document.addEventListener('keydown', this._onKeyDown);
this._sessionStore = sessionStore;
this._sessionStoreToken = this._sessionStore.addListener(
this._setStateFromSessionStore,
);
this._setStateFromSessionStore();
this._matrixClient.on("accountData", this.onAccountData);
},
componentWillUnmount: function() {
document.removeEventListener('keydown', this._onKeyDown);
this._matrixClient.removeListener("accountData", this.onAccountData);
if (this._sessionStoreToken) {
this._sessionStoreToken.remove();
}
},
// Child components assume that the client peg will not be null, so give them some
// sort of assurance here by only allowing a re-render if the client is truthy.
//
// This is required because `LoggedInView` maintains its own state and if this state
// updates after the client peg has been made null (during logout), then it will
// attempt to re-render and the children will throw errors.
shouldComponentUpdate: function() {
return Boolean(MatrixClientPeg.get());
},
getScrollStateForRoom: function(roomId) {
@ -89,6 +127,20 @@ export default React.createClass({
return this.refs.roomView.canResetTimeline();
},
_setStateFromSessionStore() {
this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
});
},
onAccountData: function(event) {
if (event.getType() === "im.vector.web.settings") {
this.setState({
useCompactLayout: event.getContent().useCompactLayout,
});
}
},
_onKeyDown: function(ev) {
/*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
@ -159,8 +211,8 @@ export default React.createClass({
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
const HomePage = sdk.getComponent('structures.HomePage');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
let page_element;
let right_panel = '';
@ -169,15 +221,12 @@ export default React.createClass({
case PageTypes.RoomView:
page_element = <RoomView
ref='roomView'
roomAddress={this.props.currentRoomAlias || this.props.currentRoomId}
autoJoin={this.props.autoJoin}
onRoomIdResolved={this.props.onRoomIdResolved}
eventId={this.props.initialEventId}
onRegistered={this.props.onRegistered}
thirdPartyInvite={this.props.thirdPartyInvite}
oobData={this.props.roomOobData}
highlightedEventId={this.props.highlightedEventId}
eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomAlias || this.props.currentRoomId}
key={this.props.currentRoomId || 'roomview'}
opacity={this.props.middleOpacity}
collapsedRhs={this.props.collapse_rhs}
ConferenceHandler={this.props.ConferenceHandler}
@ -214,12 +263,18 @@ export default React.createClass({
break;
case PageTypes.HomePage:
// If team server config is present, pass the teamServerURL. props.teamToken
// must also be set for the team page to be displayed, otherwise the
// welcomePageUrl is used (which might be undefined).
const teamServerUrl = this.props.config.teamServerConfig ?
this.props.config.teamServerConfig.teamServerURL : null;
page_element = <HomePage
collapsedRhs={this.props.collapse_rhs}
teamServerUrl={this.props.config.teamServerConfig.teamServerURL}
teamServerUrl={teamServerUrl}
teamToken={this.props.teamToken}
/>
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>
homePageUrl={this.props.config.welcomePageUrl}
/>;
break;
case PageTypes.UserView:
@ -228,16 +283,15 @@ export default React.createClass({
break;
}
const isGuest = this.props.matrixClient.isGuest();
var topBar;
if (this.props.hasNewVersion) {
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
releaseNotes={this.props.newVersionReleaseNotes}
/>;
}
else if (this.props.matrixClient.isGuest()) {
topBar = <GuestWarningBar />;
}
else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
} else if (this.state.userHasGeneratedPassword) {
topBar = <PasswordNagBar />;
} else if (!isGuest && Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
topBar = <MatrixToolbar />;
}
@ -245,6 +299,9 @@ export default React.createClass({
if (topBar) {
bodyClasses += ' mx_MatrixChat_toolbarShowing';
}
if (this.state.useCompactLayout) {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
return (
<div className='mx_MatrixChat_wrapper'>
@ -254,7 +311,6 @@ export default React.createClass({
selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapse_lhs || false}
opacity={this.props.leftOpacity}
teamToken={this.props.teamToken}
/>
<main className='mx_MatrixChat_middlePanel'>
{page_element}

View File

@ -34,11 +34,14 @@ import sdk from '../../index';
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";
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
import { _t } from '../../languageHandler';
import { _t, getCurrentLanguage } from '../../languageHandler';
module.exports = React.createClass({
displayName: 'MatrixChat',
@ -102,9 +105,6 @@ module.exports = React.createClass({
// What the LoggedInView would be showing if visible
page_type: null,
// If we are viewing a room by alias, this contains the alias
currentRoomAlias: null,
// The ID of the room we're viewing. This is either populated directly
// in the case where we view a room by ID or by RoomView when it resolves
// what ID an alias points at.
@ -128,11 +128,6 @@ module.exports = React.createClass({
hasNewVersion: false,
newVersionReleaseNotes: null,
// The username to default to when upgrading an account from a guest
upgradeUsername: null,
// The access token we had for our guest account, used when upgrading to a normal account
guestAccessToken: null,
// Parameters used in the registration dance with the IS
register_client_secret: null,
register_session_id: null,
@ -191,6 +186,9 @@ module.exports = React.createClass({
componentWillMount: function() {
SdkConfig.put(this.props.config);
RoomViewStore.addListener(this._onRoomViewStoreUpdated);
this._onRoomViewStoreUpdated();
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
// Used by _viewRoom before getting state from sync
@ -312,8 +310,6 @@ module.exports = React.createClass({
viewUserId: null,
loggedIn: false,
ready: false,
upgradeUsername: null,
guestAccessToken: null,
};
Object.assign(newState, state);
this.setState(newState);
@ -322,7 +318,6 @@ module.exports = React.createClass({
onAction: function(payload) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
switch (payload.action) {
case 'logout':
@ -349,32 +344,17 @@ module.exports = React.createClass({
screen: 'post_registration',
});
break;
case 'start_upgrade_registration':
// also stash our credentials, then if we restore the session,
// we can just do it the same way whether we started upgrade
// registration or explicitly logged out
this.setStateForNewScreen({
guestCreds: MatrixClientPeg.getCredentials(),
screen: "register",
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
guestAccessToken: MatrixClientPeg.get().getAccessToken(),
});
// stop the client: if we are syncing whilst the registration
// is completed in another browser, we'll be 401ed for using
// a guest access token for a non-guest account.
// It will be restarted in onReturnToGuestClick
Lifecycle.stopMatrixClient();
this.notifyNewScreen('register');
break;
case 'start_password_recovery':
if (this.state.loggedIn) return;
this.setStateForNewScreen({
screen: 'forgot_password',
});
this.notifyNewScreen('forgot_password');
break;
case 'start_chat':
createRoom({
dmUserId: payload.user_id,
});
break;
case 'leave_room':
this._leaveRoom(payload.room_id);
break;
@ -396,7 +376,7 @@ module.exports = React.createClass({
}, (err) => {
modal.close();
Modal.createDialog(ErrorDialog, {
title: _t('Failed to reject invitation'),
title: _t('Failed to reject invitation'),
description: err.toString(),
});
});
@ -435,37 +415,36 @@ module.exports = React.createClass({
this._viewIndexedRoom(payload.roomIndex);
break;
case 'view_user_settings':
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'view_user_settings',
},
});
dis.dispatch({action: 'view_set_mxid'});
break;
}
this._setPage(PageTypes.UserSettings);
this.notifyNewScreen('settings');
break;
case 'view_create_room':
//this._setPage(PageTypes.CreateRoom);
//this.notifyNewScreen('new');
Modal.createDialog(TextInputDialog, {
title: _t('Create Room'),
description: _t('Room name (optional)'),
button: _t('Create Room'),
onFinished: (should_create, name) => {
if (should_create) {
const createOpts = {};
if (name) createOpts.name = name;
createRoom({createOpts}).done();
}
},
});
this._createRoom();
break;
case 'view_room_directory':
this._setPage(PageTypes.RoomDirectory);
this.notifyNewScreen('directory');
break;
case 'view_home_page':
if (!this._teamToken) {
dis.dispatch({action: 'view_room_directory'});
return;
}
this._setPage(PageTypes.HomePage);
this.notifyNewScreen('home');
break;
case 'view_set_mxid':
this._setMxId(payload);
break;
case 'view_start_chat_or_reuse':
this._chatCreateOrReuse(payload.user_id);
break;
case 'view_create_chat':
this._createChat();
break;
@ -507,7 +486,11 @@ module.exports = React.createClass({
this._onSetTheme(payload.value);
break;
case 'on_logging_in':
this.setState({loggingIn: true});
// We are now logging in, so set the state to reflect that
// and also that we're not ready (we'll be marked as logged
// in once the login completes, then ready once the sync
// completes).
this.setState({loggingIn: true, ready: false});
break;
case 'on_logged_in':
this._onLoggedIn(payload.teamToken);
@ -530,6 +513,10 @@ module.exports = React.createClass({
}
},
_onRoomViewStoreUpdated: function() {
this.setState({ currentRoomId: RoomViewStore.getRoomId() });
},
_setPage: function(pageType) {
this.setState({
page_type: pageType,
@ -552,10 +539,20 @@ module.exports = React.createClass({
this.notifyNewScreen('register');
},
// TODO: Move to RoomViewStore
_viewNextRoom: function(roomIndexDelta) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
);
// If there are 0 rooms or 1 room, view the home page because otherwise
// if there are 0, we end up trying to index into an empty array, and
// if there is 1, we end up viewing the same room.
if (allRooms.length < 2) {
dis.dispatch({
action: 'view_home_page',
});
return;
}
let roomIndex = -1;
for (let i = 0; i < allRooms.length; ++i) {
if (allRooms[i].roomId == this.state.currentRoomId) {
@ -565,62 +562,62 @@ module.exports = React.createClass({
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
dis.dispatch({
action: 'view_room',
room_id: allRooms[roomIndex].roomId,
});
},
// TODO: Move to RoomViewStore
_viewIndexedRoom: function(roomIndex) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
);
if (allRooms[roomIndex]) {
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
dis.dispatch({
action: 'view_room',
room_id: allRooms[roomIndex].roomId,
});
}
},
// switch view to the given room
//
// @param {Object} room_info Object containing data about the room to be joined
// @param {string=} room_info.room_id ID of the room to join. One of room_id or room_alias must be given.
// @param {string=} room_info.room_alias Alias of the room to join. One of room_id or room_alias must be given.
// @param {boolean=} room_info.auto_join If true, automatically attempt to join the room if not already a member.
// @param {boolean=} room_info.show_settings Makes RoomView show the room settings dialog.
// @param {string=} room_info.event_id ID of the event in this room to show: this will cause a switch to the
// @param {Object} roomInfo Object containing data about the room to be joined
// @param {string=} roomInfo.room_id ID of the room to join. One of room_id or room_alias must be given.
// @param {string=} roomInfo.room_alias Alias of the room to join. One of room_id or room_alias must be given.
// @param {boolean=} roomInfo.auto_join If true, automatically attempt to join the room if not already a member.
// @param {boolean=} roomInfo.show_settings Makes RoomView show the room settings dialog.
// @param {string=} roomInfo.event_id ID of the event in this room to show: this will cause a switch to the
// context of that particular event.
// @param {Object=} room_info.third_party_invite Object containing data about the third party
// @param {boolean=} roomInfo.highlighted If true, add event_id to the hash of the URL
// and alter the EventTile to appear highlighted.
// @param {Object=} roomInfo.third_party_invite Object containing data about the third party
// we received to join the room, if any.
// @param {string=} room_info.third_party_invite.inviteSignUrl 3pid invite sign URL
// @param {string=} room_info.third_party_invite.invitedEmail The email address the invite was sent to
// @param {Object=} room_info.oob_data Object of additional data about the room
// @param {string=} roomInfo.third_party_invite.inviteSignUrl 3pid invite sign URL
// @param {string=} roomInfo.third_party_invite.invitedEmail The email address the invite was sent to
// @param {Object=} roomInfo.oob_data Object of additional data about the room
// that has been passed out-of-band (eg.
// room name and avatar from an invite email)
_viewRoom: function(room_info) {
_viewRoom: function(roomInfo) {
this.focusComposer = true;
const newState = {
initialEventId: room_info.event_id,
highlightedEventId: room_info.event_id,
initialEventPixelOffset: undefined,
page_type: PageTypes.RoomView,
thirdPartyInvite: room_info.third_party_invite,
roomOobData: room_info.oob_data,
currentRoomAlias: room_info.room_alias,
autoJoin: room_info.auto_join,
thirdPartyInvite: roomInfo.third_party_invite,
roomOobData: roomInfo.oob_data,
autoJoin: roomInfo.auto_join,
};
if (!room_info.room_alias) {
newState.currentRoomId = room_info.room_id;
}
// if we aren't given an explicit event id, look for one in the
// scrollStateMap.
//
// TODO: do this in RoomView rather than here
if (!room_info.event_id && this.refs.loggedInView) {
const scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id);
if (scrollState) {
newState.initialEventId = scrollState.focussedEvent;
newState.initialEventPixelOffset = scrollState.pixelOffset;
}
if (roomInfo.room_alias) {
console.log(
`Switching to room alias ${roomInfo.room_alias} at event ` +
roomInfo.event_id,
);
} else {
console.log(`Switching to room id ${roomInfo.room_id} at event ` +
roomInfo.event_id,
);
}
// Wait for the first sync to complete so that if a room does have an alias,
@ -628,15 +625,15 @@ module.exports = React.createClass({
let waitFor = q(null);
if (!this.firstSyncComplete) {
if (!this.firstSyncPromise) {
console.warn('Cannot view a room before first sync. room_id:', room_info.room_id);
console.warn('Cannot view a room before first sync. room_id:', roomInfo.room_id);
return;
}
waitFor = this.firstSyncPromise.promise;
}
waitFor.done(() => {
let presentedId = room_info.room_alias || room_info.room_id;
const room = MatrixClientPeg.get().getRoom(room_info.room_id);
let presentedId = roomInfo.room_alias || roomInfo.room_id;
const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
if (room) {
const theAlias = Rooms.getDisplayAliasForRoom(room);
if (theAlias) presentedId = theAlias;
@ -648,8 +645,8 @@ module.exports = React.createClass({
}
}
if (room_info.event_id) {
presentedId += "/" + room_info.event_id;
if (roomInfo.event_id && roomInfo.highlighted) {
presentedId += "/" + roomInfo.event_id;
}
this.notifyNewScreen('room/' + presentedId);
newState.ready = true;
@ -657,16 +654,135 @@ module.exports = React.createClass({
});
},
_setMxId: function(payload) {
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
const close = Modal.createDialog(SetMxIdDialog, {
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
onFinished: (submitted, credentials) => {
if (!submitted) {
dis.dispatch({
action: 'cancel_after_sync_prepared',
});
if (payload.go_home_on_cancel) {
dis.dispatch({
action: 'view_home_page',
});
}
return;
}
this.onRegistered(credentials);
},
onDifferentServerClicked: (ev) => {
dis.dispatch({action: 'start_registration'});
close();
},
onLoginClick: (ev) => {
dis.dispatch({action: 'start_login'});
close();
},
}).close;
},
_createChat: function() {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'view_create_chat',
},
});
dis.dispatch({action: 'view_set_mxid'});
return;
}
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, {
title: _t('Start a chat'),
description: _t("Who would you like to communicate with?"),
placeholder: _t("Email, name or matrix ID"),
button: _t("Start Chat")
button: _t("Start Chat"),
});
},
_createRoom: function() {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'view_create_room',
},
});
dis.dispatch({action: 'view_set_mxid'});
return;
}
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
Modal.createDialog(TextInputDialog, {
title: _t('Create Room'),
description: _t('Room name (optional)'),
button: _t('Create Room'),
onFinished: (shouldCreate, name) => {
if (shouldCreate) {
const createOpts = {};
if (name) createOpts.name = name;
createRoom({createOpts}).done();
}
},
});
},
_chatCreateOrReuse: function(userId) {
const ChatCreateOrReuseDialog = sdk.getComponent(
'views.dialogs.ChatCreateOrReuseDialog',
);
// Use a deferred action to reshow the dialog once the user has registered
if (MatrixClientPeg.get().isGuest()) {
// No point in making 2 DMs with welcome bot. This assumes view_set_mxid will
// result in a new DM with the welcome user.
if (userId !== this.props.config.welcomeUserId) {
dis.dispatch({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'view_start_chat_or_reuse',
user_id: userId,
},
});
}
dis.dispatch({
action: 'view_set_mxid',
// If the set_mxid dialog is cancelled, view /home because if the browser
// was pointing at /user/@someone:domain?action=chat, the URL needs to be
// reset so that they can revisit /user/.. // (and trigger
// `_chatCreateOrReuse` again)
go_home_on_cancel: true,
});
return;
}
const close = Modal.createDialog(ChatCreateOrReuseDialog, {
userId: userId,
onFinished: (success) => {
if (!success) {
// Dialog cancelled, default to home
dis.dispatch({ action: 'view_home_page' });
}
},
onNewDMClick: () => {
dis.dispatch({
action: 'start_chat',
user_id: userId,
});
// Close the dialog, indicate success (calls onFinished(true))
close(true);
},
onExistingRoomSelected: (roomId) => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
close(true);
},
}).close;
},
_invite: function(roomId) {
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, {
@ -684,8 +800,12 @@ module.exports = React.createClass({
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
Modal.createDialog(QuestionDialog, {
title: "Leave room",
description: <span>Are you sure you want to leave the room <i>{roomToLeave.name}</i>?</span>,
title: _t("Leave room"),
description: (
<span>
{_t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name})}
</span>
),
onFinished: (shouldLeave) => {
if (shouldLeave) {
const d = MatrixClientPeg.get().leave(roomId);
@ -696,7 +816,7 @@ module.exports = React.createClass({
d.then(() => {
modal.close();
if (this.currentRoomId === roomId) {
if (this.state.currentRoomId === roomId) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
@ -791,12 +911,27 @@ module.exports = React.createClass({
this._teamToken = teamToken;
dis.dispatch({action: 'view_home_page'});
} else if (this._is_registered) {
if (this.props.config.welcomeUserId) {
createRoom({dmUserId: this.props.config.welcomeUserId});
this._is_registered = false;
// reset the 'have completed first sync' flag,
// since we've just logged in and will be about to sync
this.firstSyncComplete = false;
this.firstSyncPromise = q.defer();
// Set the display name = user ID localpart
MatrixClientPeg.get().setDisplayName(
MatrixClientPeg.get().getUserIdLocalpart(),
);
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
createRoom({
dmUserId: this.props.config.welcomeUserId,
// Only view the welcome user if we're NOT looking at a room
andView: !this.state.currentRoomId,
});
return;
}
// The user has just logged in after registering
dis.dispatch({action: 'view_room_directory'});
dis.dispatch({action: 'view_home_page'});
} else {
this._showScreenAfterLogin();
}
@ -818,12 +953,8 @@ module.exports = React.createClass({
action: 'view_room',
room_id: localStorage.getItem('mx_last_room_id'),
});
} else if (this._teamToken) {
// Team token might be set if we're a guest.
// Guests do not call _onLoggedIn with a teamToken
dis.dispatch({action: 'view_home_page'});
} else {
dis.dispatch({action: 'view_room_directory'});
dis.dispatch({action: 'view_home_page'});
}
},
@ -837,7 +968,6 @@ module.exports = React.createClass({
ready: false,
collapse_lhs: false,
collapse_rhs: false,
currentRoomAlias: null,
currentRoomId: null,
page_type: PageTypes.RoomDirectory,
});
@ -875,6 +1005,12 @@ module.exports = React.createClass({
});
cli.on('sync', function(state, prevState) {
// LifecycleStore and others cannot directly subscribe to matrix client for
// events because flux only allows store state changes during flux dispatches.
// So dispatch directly from here. Ideally we'd use a SyncStateStore that
// would do this dispatch and expose the sync state itself (by listening to
// its own dispatch).
dis.dispatch({action: 'sync_state', prevState, state});
self.updateStatusIndicator(state, prevState);
if (state === "SYNCING" && prevState === "SYNCING") {
return;
@ -944,6 +1080,11 @@ module.exports = React.createClass({
dis.dispatch({
action: 'view_home_page',
});
} else if (screen == 'start') {
this.showScreen('home');
dis.dispatch({
action: 'view_set_mxid',
});
} else if (screen == 'directory') {
dis.dispatch({
action: 'view_room_directory',
@ -971,6 +1112,10 @@ module.exports = React.createClass({
const payload = {
action: 'view_room',
event_id: eventId,
// If an event ID is given in the URL hash, notify RoomViewStore to mark
// it as highlighted, which will propagate to RoomView and highlight the
// associated EventTile.
highlighted: Boolean(eventId),
third_party_invite: thirdPartyInvite,
oob_data: oobData,
};
@ -987,6 +1132,12 @@ module.exports = React.createClass({
}
} else if (screen.indexOf('user/') == 0) {
const userId = screen.substring(5);
if (params.action === 'chat') {
this._chatCreateOrReuse(userId);
return;
}
this.setState({ viewUserId: userId });
this._setPage(PageTypes.UserView);
this.notifyNewScreen('user/' + userId);
@ -1083,6 +1234,8 @@ module.exports = React.createClass({
},
onRegistered: function(credentials, teamToken) {
// XXX: These both should be in state or ideally store(s) because we risk not
// rendering the most up-to-date view of state otherwise.
// teamToken may not be truthy
this._teamToken = teamToken;
this._is_registered = true;
@ -1126,7 +1279,15 @@ module.exports = React.createClass({
PlatformPeg.get().setNotificationCount(notifCount);
}
document.title = `Riot ${state === "ERROR" ? " [offline]" : ""}${notifCount > 0 ? ` [${notifCount}]` : ""}`;
let title = "Riot ";
if (state === "ERROR") {
title += `[${_t("Offline")}] `;
}
if (notifCount > 0) {
title += `[${notifCount}]`;
}
document.title = title;
},
onUserSettingsClose: function() {
@ -1139,18 +1300,11 @@ module.exports = React.createClass({
});
} else {
dis.dispatch({
action: 'view_room_directory',
action: 'view_home_page',
});
}
},
onRoomIdResolved: function(roomId) {
// It's the RoomView's resposibility to look up room aliases, but we need the
// ID to pass into things like the Member List, so the Room View tells us when
// its done that resolution so we can display things that take a room ID.
this.setState({currentRoomId: roomId});
},
_makeRegistrationUrl: function(params) {
if (this.props.startingFragmentQueryParams.referrer) {
params.referrer = this.props.startingFragmentQueryParams.referrer;
@ -1192,9 +1346,10 @@ module.exports = React.createClass({
const LoggedInView = sdk.getComponent('structures.LoggedInView');
return (
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
onRoomIdResolved={this.onRoomIdResolved}
onRoomCreated={this.onRoomCreated}
onUserSettingsClose={this.onUserSettingsClose}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
teamToken={this._teamToken}
{...this.props}
{...this.state}
@ -1220,8 +1375,6 @@ module.exports = React.createClass({
idSid={this.state.register_id_sid}
email={this.props.startingFragmentQueryParams.email}
referrer={this.props.startingFragmentQueryParams.referrer}
username={this.state.upgradeUsername}
guestAccessToken={this.state.guestAccessToken}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand}

View File

@ -91,6 +91,9 @@ module.exports = React.createClass({
// show timestamps always
alwaysShowTimestamps: React.PropTypes.bool,
// hide redacted events as per old behaviour
hideRedactions: React.PropTypes.bool,
},
componentWillMount: function() {
@ -353,7 +356,7 @@ module.exports = React.createClass({
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1}/></li>;
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
ret.push(dateSeparator);
}
@ -495,11 +498,13 @@ module.exports = React.createClass({
// do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) {
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
ret.push(dateSeparator);
continuation = false;
}
if (mxEv.isRedacted() && this.props.hideRedactions) return ret;
var eventId = mxEv.getId();
var highlight = (eventId == this.props.highlightedEventId);

View File

@ -15,9 +15,8 @@ limitations under the License.
*/
import React from 'react';
import { _t } from '../../languageHandler';
import { _t, _tJsx } from '../../languageHandler';
import sdk from '../../index';
import dis from '../../dispatcher';
import WhoIsTyping from '../../WhoIsTyping';
import MatrixClientPeg from '../../MatrixClientPeg';
import MemberAvatar from '../views/avatars/MemberAvatar';
@ -282,14 +281,13 @@ module.exports = React.createClass({
{ this.props.unsentMessageError }
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
<a className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onResendAllClick }>
{_t('Resend all')}
</a> {_t('or')} <a
className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onCancelAllClick }>
{_t('cancel all')}
</a> {_t('now. You can also select individual messages to resend or cancel.')}
{_tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
[
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={ this.props.onResendAllClick }>{sub}</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={ this.props.onCancelAllClick }>{sub}</a>,
]
)}
</div>
</div>
);
@ -298,8 +296,8 @@ module.exports = React.createClass({
// unread count trumps who is typing since the unread count is only
// set when you've scrolled up
if (this.props.numUnreadMessages) {
var unreadMsgs = this.props.numUnreadMessages + " new message" +
(this.props.numUnreadMessages > 1 ? "s" : "");
// MUST use var name "count" for pluralization to kick in
var unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
return (
<div className="mx_RoomStatusBar_unreadMessagesBar"

View File

@ -45,6 +45,8 @@ import KeyCode from '../../KeyCode';
import UserProvider from '../../autocomplete/UserProvider';
import RoomViewStore from '../../stores/RoomViewStore';
var DEBUG = false;
if (DEBUG) {
@ -59,16 +61,9 @@ module.exports = React.createClass({
propTypes: {
ConferenceHandler: React.PropTypes.any,
// Either a room ID or room alias for the room to display.
// If the room is being displayed as a result of the user clicking
// on a room alias, the alias should be supplied. Otherwise, a room
// ID should be supplied.
roomAddress: React.PropTypes.string.isRequired,
// If a room alias is passed to roomAddress, a function can be
// provided here that will be called with the ID of the room
// once it has been resolved.
onRoomIdResolved: React.PropTypes.func,
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: React.PropTypes.func,
// An object representing a third party invite to join this room
// Fields:
@ -88,36 +83,8 @@ module.exports = React.createClass({
// * invited us tovthe room
oobData: React.PropTypes.object,
// id of an event to jump to. If not given, will go to the end of the
// live timeline.
eventId: React.PropTypes.string,
// where to position the event given by eventId, in pixels from the
// bottom of the viewport. If not given, will try to put the event
// 1/3 of the way down the viewport.
eventPixelOffset: React.PropTypes.number,
// ID of an event to highlight. If undefined, no event will be highlighted.
// Typically this will either be the same as 'eventId', or undefined.
highlightedEventId: React.PropTypes.string,
// is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool,
// a map from room id to scroll state, which will be updated on unmount.
//
// If there is no special scroll state (ie, we are following the live
// timeline), the scroll state is null. Otherwise, it is an object with
// the following properties:
//
// focussedEvent: the ID of the 'focussed' event. Typically this is
// the last event fully visible in the viewport, though if we
// have done an explicit scroll to an explicit event, it will be
// that event.
//
// pixelOffset: the number of pixels the window is scrolled down
// from the focussedEvent.
scrollStateMap: React.PropTypes.object,
},
getInitialState: function() {
@ -125,6 +92,14 @@ module.exports = React.createClass({
room: null,
roomId: null,
roomLoading: true,
peekLoading: false,
// The event to be scrolled to initially
initialEventId: null,
// The offset in pixels from the event with which to scroll vertically
initialEventPixelOffset: null,
// Whether to highlight the event scrolled to
isInitialEventHighlighted: null,
forwardingEvent: null,
editingRoomSettings: false,
@ -172,40 +147,47 @@ module.exports = React.createClass({
onClickCompletes: true,
onStateChange: (isCompleting) => {
this.forceUpdate();
}
},
});
if (this.props.roomAddress[0] == '#') {
// we always look up the alias from the directory server:
// we want the room that the given alias is pointing to
// right now. We may have joined that alias before but there's
// no guarantee the alias hasn't subsequently been remapped.
MatrixClientPeg.get().getRoomIdForAlias(this.props.roomAddress).done((result) => {
if (this.props.onRoomIdResolved) {
this.props.onRoomIdResolved(result.room_id);
}
var room = MatrixClientPeg.get().getRoom(result.room_id);
this.setState({
room: room,
roomId: result.room_id,
roomLoading: !room,
unsentMessageError: this._getUnsentMessageError(room),
}, this._onHaveRoom);
}, (err) => {
this.setState({
roomLoading: false,
roomLoadError: err,
});
});
} else {
var room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
this.setState({
roomId: this.props.roomAddress,
room: room,
roomLoading: !room,
unsentMessageError: this._getUnsentMessageError(room),
}, this._onHaveRoom);
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true);
},
_onRoomViewStoreUpdate: function(initial) {
if (this.unmounted) {
return;
}
const newState = {
roomId: RoomViewStore.getRoomId(),
roomAlias: RoomViewStore.getRoomAlias(),
roomLoading: RoomViewStore.isRoomLoading(),
roomLoadError: RoomViewStore.getRoomLoadError(),
joining: RoomViewStore.isJoining(),
initialEventId: RoomViewStore.getInitialEventId(),
initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
};
// Clear the search results when clicking a search result (which changes the
// currently scrolled to event, this.state.initialEventId).
if (this.state.initialEventId !== newState.initialEventId) {
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, this.state.roomId could be null (e.g. the alias might not
// have been resolved yet) so anything called here must handle this case.
this._onHaveRoom();
this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId));
});
},
_onHaveRoom: function() {
@ -223,26 +205,29 @@ module.exports = React.createClass({
// NB. We peek if we are not in the room, although if we try to peek into
// a room in which we have a member event (ie. we've left) synapse will just
// send us the same data as we get in the sync (ie. the last events we saw).
var user_is_in_room = null;
if (this.state.room) {
user_is_in_room = this.state.room.hasMembershipState(
MatrixClientPeg.get().credentials.userId, 'join'
const room = MatrixClientPeg.get().getRoom(this.state.roomId);
let isUserJoined = null;
if (room) {
isUserJoined = room.hasMembershipState(
MatrixClientPeg.get().credentials.userId, 'join',
);
this._updateAutoComplete();
this.tabComplete.loadEntries(this.state.room);
this._updateAutoComplete(room);
this.tabComplete.loadEntries(room);
}
if (!user_is_in_room && this.state.roomId) {
if (!isUserJoined && !this.state.joining && this.state.roomId) {
if (this.props.autoJoin) {
this.onJoinButtonClicked();
} else if (this.state.roomId) {
console.log("Attempting to peek into room %s", this.state.roomId);
this.setState({
peekLoading: true,
});
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
this.setState({
room: room,
roomLoading: false,
peekLoading: false,
});
this._onRoomLoaded(room);
}, (err) => {
@ -252,16 +237,19 @@ module.exports = React.createClass({
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
// This is fine: the room just isn't peekable (we assume).
this.setState({
roomLoading: false,
peekLoading: false,
});
} else {
throw err;
}
}).done();
}
} else if (user_is_in_room) {
} else if (isUserJoined) {
MatrixClientPeg.get().stopPeeking();
this._onRoomLoaded(this.state.room);
this.setState({
unsentMessageError: this._getUnsentMessageError(room),
});
this._onRoomLoaded(room);
}
},
@ -297,17 +285,6 @@ module.exports = React.createClass({
}
},
componentWillReceiveProps: function(newProps) {
if (newProps.roomAddress != this.props.roomAddress) {
throw new Error(_t("changing room on a RoomView is not supported"));
}
if (newProps.eventId != this.props.eventId) {
// when we change focussed event id, hide the search results.
this.setState({searchResults: null});
}
},
shouldComponentUpdate: function(nextProps, nextState) {
return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
!ObjectUtils.shallowEqual(this.state, nextState));
@ -333,7 +310,7 @@ module.exports = React.createClass({
this.unmounted = true;
// update the scroll map before we get unmounted
this._updateScrollMap();
this._updateScrollMap(this.state.roomId);
if (this.refs.roomView) {
// disconnect the D&D event listeners from the room view. This
@ -362,6 +339,11 @@ module.exports = React.createClass({
document.removeEventListener("keydown", this.onKeyDown);
// Remove RoomStore listener
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
// cancel any pending calls to the rate_limited_funcs
this._updateRoomMembers.cancelPendingCall();
@ -527,7 +509,7 @@ module.exports = React.createClass({
this._updatePreviewUrlVisibility(room);
},
_warnAboutEncryption: function (room) {
_warnAboutEncryption: function(room) {
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
return;
}
@ -607,21 +589,27 @@ module.exports = React.createClass({
});
},
onRoom: function(room) {
// This event is fired when the room is 'stored' by the JS SDK, which
// means it's now a fully-fledged room object ready to be used, so
// set it in our state and start using it (ie. init the timeline)
// This will happen if we start off viewing a room we're not joined,
// then join it whilst RoomView is looking at that room.
if (!this.state.room && room.roomId == this._joiningRoomId) {
this._joiningRoomId = undefined;
this.setState({
room: room,
joining: false,
});
this._onRoomLoaded(room);
_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,
}, () => {
this._onRoomLoaded(room);
});
},
updateTint: function() {
@ -687,7 +675,7 @@ module.exports = React.createClass({
// refresh the tab complete list
this.tabComplete.loadEntries(this.state.room);
this._updateAutoComplete();
this._updateAutoComplete(this.state.room);
// if we are now a member of the room, where we were not before, that
// means we have finished joining a room we were previously peeking
@ -704,10 +692,6 @@ module.exports = React.createClass({
// compatability workaround, let's not bother.
Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done();
}
this.setState({
joining: false
});
}
}, 500),
@ -716,7 +700,7 @@ module.exports = React.createClass({
if (!unsentMessages.length) return "";
for (const event of unsentMessages) {
if (!event.error || event.error.name !== "UnknownDeviceError") {
return _t("Some of your messages have not been sent") + ".";
return _t("Some of your messages have not been sent.");
}
}
return _t("Message not sent due to unknown devices being present");
@ -782,41 +766,62 @@ module.exports = React.createClass({
},
onJoinButtonClicked: function(ev) {
var self = this;
const cli = MatrixClientPeg.get();
var cli = MatrixClientPeg.get();
var display_name_promise = q();
// if this is the first room we're joining, check the user has a display name
// and if they don't, prompt them to set one.
// NB. This unfortunately does not re-use the ChangeDisplayName component because
// it doesn't behave quite as desired here (we want an input field here rather than
// content-editable, and we want a default).
if (cli.getRooms().filter((r) => {
return r.hasMembershipState(cli.credentials.userId, "join");
})) {
display_name_promise = cli.getProfileInfo(cli.credentials.userId).then((result) => {
if (!result.displayname) {
var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
var dialog_defer = q.defer();
Modal.createDialog(SetDisplayNameDialog, {
currentDisplayName: result.displayname,
onFinished: (submitted, newDisplayName) => {
if (submitted) {
cli.setDisplayName(newDisplayName).done(() => {
dialog_defer.resolve();
});
}
else {
dialog_defer.reject();
}
}
});
return dialog_defer.promise;
}
// If the user is a ROU, allow them to transition to a PWLU
if (cli && cli.isGuest()) {
// Join this room once the user has registered and logged in
const signUrl = this.props.thirdPartyInvite ?
this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'join_room',
opts: { inviteSignUrl: signUrl },
},
});
// Don't peek whilst registering otherwise getPendingEventList complains
// Do this by indicating our intention to join
dis.dispatch({
action: 'will_join',
});
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
const close = Modal.createDialog(SetMxIdDialog, {
homeserverUrl: cli.getHomeserverUrl(),
onFinished: (submitted, credentials) => {
if (submitted) {
this.props.onRegistered(credentials);
} else {
dis.dispatch({
action: 'cancel_after_sync_prepared',
});
dis.dispatch({
action: 'cancel_join',
});
}
},
onDifferentServerClicked: (ev) => {
dis.dispatch({action: 'start_registration'});
close();
},
onLoginClick: (ev) => {
dis.dispatch({action: 'start_login'});
close();
},
}).close;
return;
}
display_name_promise.then(() => {
q().then(() => {
const signUrl = this.props.thirdPartyInvite ?
this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({
action: 'join_room',
opts: { inviteSignUrl: signUrl },
});
// if this is an invite and has the 'direct' hint set, mark it as a DM room now.
if (this.state.room) {
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
@ -828,72 +833,7 @@ module.exports = React.createClass({
}
}
}
return q();
}).then(() => {
var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined;
return MatrixClientPeg.get().joinRoom(this.props.roomAddress,
{ inviteSignUrl: sign_url } );
}).then(function(resp) {
var roomId = resp.roomId;
// It is possible that there is no Room yet if state hasn't come down
// from /sync - joinRoom will resolve when the HTTP request to join succeeds,
// NOT when it comes down /sync. If there is no room, we'll keep the
// joining flag set until we see it.
// We'll need to initialise the timeline when joining, but due to
// the above, we can't do it here: we do it in onRoom instead,
// once we have a useable room object.
var room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
// wait for the room to turn up in onRoom.
self._joiningRoomId = roomId;
} else {
// we've got a valid room, but that might also just mean that
// it was peekable (so we had one before anyway). If we are
// not yet a member of the room, we will need to wait for that
// to happen, in onRoomStateMember.
var me = MatrixClientPeg.get().credentials.userId;
self.setState({
joining: !room.hasMembershipState(me, "join"),
room: room
});
}
}).catch(function(error) {
self.setState({
joining: false,
joinError: error
});
if (!error) return;
// https://matrix.org/jira/browse/SYN-659
// Need specific error message if joining a room is refused because the user is a guest and guest access is not allowed
if (
error.errcode == 'M_GUEST_ACCESS_FORBIDDEN' ||
(
error.errcode == 'M_FORBIDDEN' &&
MatrixClientPeg.get().isGuest()
)
) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: _t("Failed to join the room"),
description: _t("This room is private or inaccessible to guests. You may be able to join if you register") + "."
});
} else {
var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Failed to join room"),
description: msg,
});
}
}).done();
this.setState({
joining: true
});
},
@ -945,11 +885,7 @@ module.exports = React.createClass({
uploadFile: function(file) {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: _t("Please Register"),
description: _t("Guest users can't upload files. Please register to upload") + "."
});
dis.dispatch({action: 'view_set_mxid'});
return;
}
@ -1307,21 +1243,6 @@ module.exports = React.createClass({
}
},
// update scrollStateMap on unmount
_updateScrollMap: function() {
if (!this.state.room) {
// we were instantiated on a room alias and haven't yet joined the room.
return;
}
if (!this.props.scrollStateMap) return;
var roomId = this.state.room.roomId;
var state = this._getScrollState();
this.props.scrollStateMap[roomId] = state;
},
// get the current scroll position of the room, so that it can be
// restored when we switch back to it.
//
@ -1474,9 +1395,9 @@ module.exports = React.createClass({
}
},
_updateAutoComplete: function() {
_updateAutoComplete: function(room) {
const myUserId = MatrixClientPeg.get().credentials.userId;
const members = this.state.room.getJoinedMembers().filter(function(member) {
const members = room.getJoinedMembers().filter(function(member) {
if (member.userId !== myUserId) return true;
});
UserProvider.getInstance().setUserList(members);
@ -1496,7 +1417,7 @@ module.exports = React.createClass({
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
if (!this.state.room) {
if (this.state.roomLoading) {
if (this.state.roomLoading || this.state.peekLoading) {
return (
<div className="mx_RoomView">
<Loader />
@ -1514,7 +1435,7 @@ module.exports = React.createClass({
// We have no room object for this room, only the ID.
// We've got to this room by following a link, possibly a third party invite.
var room_alias = this.props.roomAddress[0] == '#' ? this.props.roomAddress : null;
var room_alias = this.state.room_alias;
return (
<div className="mx_RoomView">
<RoomHeader ref="header"
@ -1744,6 +1665,14 @@ module.exports = React.createClass({
hideMessagePanel = true;
}
const shouldHighlight = this.state.isInitialEventHighlighted;
let highlightedEventId = null;
if (this.state.forwardingEvent) {
highlightedEventId = this.state.forwardingEvent.getId();
} else if (shouldHighlight) {
highlightedEventId = this.state.initialEventId;
}
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
var messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef}
@ -1751,9 +1680,9 @@ module.exports = React.createClass({
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
manageReadMarkers={true}
hidden={hideMessagePanel}
highlightedEventId={this.state.forwardingEvent ? this.state.forwardingEvent.getId() : this.props.highlightedEventId}
eventId={this.props.eventId}
eventPixelOffset={this.props.eventPixelOffset}
highlightedEventId={highlightedEventId}
eventId={this.state.initialEventId}
eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={ this.onMessageListScroll }
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
showUrlPreview = { this.state.showUrlPreview }

View File

@ -131,6 +131,8 @@ var TimelinePanel = React.createClass({
}
}
const syncedSettings = UserSettingsStore.getSyncedSettings();
return {
events: [],
timelineLoading: true, // track whether our room timeline is loading
@ -175,10 +177,13 @@ var TimelinePanel = React.createClass({
clientSyncState: MatrixClientPeg.get().getSyncState(),
// should the event tiles have twelve hour times
isTwelveHour: UserSettingsStore.getSyncedSetting('showTwelveHourTimestamps'),
isTwelveHour: syncedSettings.showTwelveHourTimestamps,
// always show timestamps on event tiles?
alwaysShowTimestamps: UserSettingsStore.getSyncedSetting('alwaysShowTimestamps'),
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
// hide redacted events as per old behaviour
hideRedactions: syncedSettings.hideRedactions,
};
},
@ -897,6 +902,9 @@ var TimelinePanel = React.createClass({
var onError = (error) => {
this.setState({timelineLoading: false});
console.error(
`Error loading timeline panel at ${eventId}: ${error}`,
);
var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -915,9 +923,9 @@ var TimelinePanel = React.createClass({
});
};
}
var message = (error.errcode == 'M_FORBIDDEN')
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question") + "."
: _t("Tried to load a specific point in this room's timeline, but was unable to find it") + ".";
var message = (error.errcode == 'M_FORBIDDEN')
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
Modal.createDialog(ErrorDialog, {
title: _t("Failed to load timeline position"),
description: message,
@ -1113,26 +1121,27 @@ var TimelinePanel = React.createClass({
);
return (
<MessagePanel ref="messagePanel"
hidden={ this.props.hidden }
backPaginating={ this.state.backPaginating }
forwardPaginating={ forwardPaginating }
events={ this.state.events }
highlightedEventId={ this.props.highlightedEventId }
readMarkerEventId={ this.state.readMarkerEventId }
readMarkerVisible={ this.state.readMarkerVisible }
suppressFirstDateSeparator={ this.state.canBackPaginate }
showUrlPreview = { this.props.showUrlPreview }
manageReadReceipts = { this.props.manageReadReceipts }
ourUserId={ MatrixClientPeg.get().credentials.userId }
stickyBottom={ stickyBottom }
onScroll={ this.onMessageListScroll }
onFillRequest={ this.onMessageListFillRequest }
onUnfillRequest={ this.onMessageListUnfillRequest }
opacity={ this.props.opacity }
isTwelveHour={ this.state.isTwelveHour }
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
className={ this.props.className }
tileShape={ this.props.tileShape }
hidden={ this.props.hidden }
hideRedactions={ this.state.hideRedactions }
backPaginating={ this.state.backPaginating }
forwardPaginating={ forwardPaginating }
events={ this.state.events }
highlightedEventId={ this.props.highlightedEventId }
readMarkerEventId={ this.state.readMarkerEventId }
readMarkerVisible={ this.state.readMarkerVisible }
suppressFirstDateSeparator={ this.state.canBackPaginate }
showUrlPreview = { this.props.showUrlPreview }
manageReadReceipts = { this.props.manageReadReceipts }
ourUserId={ MatrixClientPeg.get().credentials.userId }
stickyBottom={ stickyBottom }
onScroll={ this.onMessageListScroll }
onFillRequest={ this.onMessageListFillRequest }
onUnfillRequest={ this.onMessageListUnfillRequest }
opacity={ this.props.opacity }
isTwelveHour={ this.state.isTwelveHour }
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
className={ this.props.className }
tileShape={ this.props.tileShape }
/>
);
},

View File

@ -18,6 +18,7 @@ var React = require('react');
var ContentMessages = require('../../ContentMessages');
var dis = require('../../dispatcher');
var filesize = require('filesize');
import { _t } from '../../languageHandler';
module.exports = React.createClass({displayName: 'UploadBar',
propTypes: {
@ -81,10 +82,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
uploadedSize = uploadedSize.replace(/ .*/, '');
}
var others;
if (uploads.length > 1) {
others = ' and ' + (uploads.length - 1) + ' other' + (uploads.length > 2 ? 's' : '');
}
// MUST use var name 'count' for pluralization to kick in
var uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)});
return (
<div className="mx_UploadBar">
@ -98,7 +97,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
<div className="mx_UploadBar_uploadBytes">
{ uploadedSize } / { totalSize }
</div>
<div className="mx_UploadBar_uploadFilename">Uploading {upload.fileName}{others}</div>
<div className="mx_UploadBar_uploadFilename">{uploadText}</div>
</div>
);
}

View File

@ -24,6 +24,7 @@ const dis = require("../../dispatcher");
const q = require('q');
const packageJson = require('../../../package.json');
const UserSettingsStore = require('../../UserSettingsStore');
const CallMediaHandler = require('../../CallMediaHandler');
const GeminiScrollbar = require('react-gemini-scrollbar');
const Email = require('../../email');
const AddThreepid = require('../../AddThreepid');
@ -82,12 +83,20 @@ const SETTINGS_LABELS = [
{
id: 'hideJoinLeaves',
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
}
/*
},
{
id: 'useCompactLayout',
label: 'Use compact timeline layout',
},
{
id: 'hideRedactions',
label: 'Hide removed messages',
},
{
id: 'disableMarkdown',
label: 'Disable markdown formatting',
},
/*
{
id: 'useFixedWidthFont',
label: 'Use fixed width font',
@ -180,6 +189,7 @@ module.exports = React.createClass({
email_add_pending: false,
vectorVersion: undefined,
rejectingInvites: false,
mediaDevices: null,
};
},
@ -200,6 +210,8 @@ module.exports = React.createClass({
});
}
this._refreshMediaDevices();
// Bulk rejecting invites:
// /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms()
// will still return rooms with invites. To get around this, add a listener for
@ -261,6 +273,20 @@ module.exports = React.createClass({
this.setState({ electron_settings: settings });
},
_refreshMediaDevices: function() {
q().then(() => {
return CallMediaHandler.getDevices();
}).then((mediaDevices) => {
// console.log("got mediaDevices", mediaDevices, this._unmounted);
if (this._unmounted) return;
this.setState({
mediaDevices,
activeAudioInput: this._localSettings['webrtc_audioinput'],
activeVideoInput: this._localSettings['webrtc_videoinput'],
});
});
},
_refreshFromServer: function() {
const self = this;
q.all([
@ -288,15 +314,6 @@ module.exports = React.createClass({
},
onAvatarPickerClick: function(ev) {
if (MatrixClientPeg.get().isGuest()) {
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: _t("Please Register"),
description: _t("Guests can't set avatars. Please register."),
});
return;
}
if (this.refs.file_label) {
this.refs.file_label.click();
}
@ -371,14 +388,12 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Success"),
description: _t("Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them") + ".",
});
},
onUpgradeClicked: function() {
dis.dispatch({
action: "start_upgrade_registration",
description: _t(
"Your password was successfully changed. You will not receive " +
"push notifications on other devices until you log back in to them",
) + ".",
});
dis.dispatch({action: 'password_changed'});
},
onEnableNotificationsChange: function(event) {
@ -408,7 +423,10 @@ module.exports = React.createClass({
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
Modal.createDialog(QuestionDialog, {
title: _t("Verification Pending"),
description: _t("Please check your email and click on the link it contains. Once this is done, click continue."),
description: _t(
"Please check your email and click on the link it contains. Once this " +
"is done, click continue.",
),
button: _t('Continue'),
onFinished: this.onEmailDialogFinished,
});
@ -428,7 +446,7 @@ module.exports = React.createClass({
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: _t("Remove Contact Information?"),
description: _t("Remove %(threePid)s?", { threePid : threepid.address }),
description: _t("Remove %(threePid)s?", { threePid: threepid.address }),
button: _t('Remove'),
onFinished: (submit) => {
if (submit) {
@ -470,8 +488,8 @@ module.exports = React.createClass({
this.setState({email_add_pending: false});
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let message = _t("Unable to verify email address.") + " " +
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
const message = _t("Unable to verify email address.") + " " +
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
Modal.createDialog(QuestionDialog, {
title: _t("Verification Pending"),
description: message,
@ -589,7 +607,7 @@ module.exports = React.createClass({
}
},
_renderLanguageSetting: function () {
_renderLanguageSetting: function() {
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
return <div>
<label htmlFor="languageSelector">{_t('Interface Language')}</label>
@ -620,7 +638,7 @@ module.exports = React.createClass({
<input id="urlPreviewsDisabled"
type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
onChange={ this._onPreviewsDisabledChanged }
/>
<label htmlFor="urlPreviewsDisabled">
{ _t("Disable inline URL previews by default") }
@ -628,17 +646,24 @@ module.exports = React.createClass({
</div>;
},
_onPreviewsDisabledChanged: function(e) {
UserSettingsStore.setUrlPreviewsDisabled(e.target.checked);
},
_renderSyncedSetting: function(setting) {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = (e) => {
UserSettingsStore.setSyncedSetting(setting.id, e.target.checked);
if (setting.fn) setting.fn(e.target.checked);
};
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
type="checkbox"
defaultChecked={ this._syncedSettings[setting.id] }
onChange={
(e) => {
UserSettingsStore.setSyncedSetting(setting.id, e.target.checked);
if (setting.fn) setting.fn(e.target.checked);
}
}
onChange={ onChange }
/>
<label htmlFor={ setting.id }>
{ _t(setting.label) }
@ -647,22 +672,24 @@ module.exports = React.createClass({
},
_renderThemeSelector: function(setting) {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = (e) => {
if (e.target.checked) {
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
}
dis.dispatch({
action: 'set_theme',
value: setting.value,
});
};
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
<input id={ setting.id + "_" + setting.value }
type="radio"
name={ setting.id }
value={ setting.value }
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
onChange={ (e) => {
if (e.target.checked) {
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
}
dis.dispatch({
action: 'set_theme',
value: setting.value,
});
}
}
onChange={ onChange }
/>
<label htmlFor={ setting.id + "_" + setting.value }>
{ setting.label }
@ -701,8 +728,10 @@ module.exports = React.createClass({
<h3>{ _t("Cryptography") }</h3>
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
<ul>
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
<li><label>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>
@ -714,16 +743,18 @@ module.exports = React.createClass({
},
_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
const onChange = (e) => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
if (setting.fn) setting.fn(e.target.checked);
};
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
type="checkbox"
defaultChecked={ this._localSettings[setting.id] }
onChange={
(e) => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
if (setting.fn) setting.fn(e.target.checked);
}
}
onChange={ onChange }
/>
<label htmlFor={ setting.id }>
{ _t(setting.label) }
@ -735,7 +766,7 @@ module.exports = React.createClass({
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return (
<div>
<h3>Devices</h3>
<h3>{_t("Devices")}</h3>
<DevicesPanel className="mx_UserSettings_section"/>
</div>
);
@ -759,6 +790,8 @@ module.exports = React.createClass({
},
_renderAnalyticsControl: function() {
if (!SdkConfig.get().piwik) return <div/>;
return <div>
<h3>{ _t('Analytics') }</h3>
<div className="mx_UserSettings_section">
@ -771,31 +804,29 @@ module.exports = React.createClass({
_renderLabs: function() {
// default to enabled if undefined
if (this.props.enableLabs === false) return null;
UserSettingsStore.doTranslations();
const features = UserSettingsStore.LABS_FEATURES.map((feature) => (
<div key={feature.id} className="mx_UserSettings_toggle">
<input
type="checkbox"
id={feature.id}
name={feature.id}
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
onChange={(e) => {
if (MatrixClientPeg.get().isGuest()) {
e.target.checked = false;
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: _t("Please Register"),
description: _t("Guests can't use labs features. Please register."),
});
return;
}
const features = UserSettingsStore.LABS_FEATURES.map((feature) => {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = (e) => {
UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked);
this.forceUpdate();
};
UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked);
this.forceUpdate();
}}/>
<label htmlFor={feature.id}>{feature.name}</label>
</div>
));
return (
<div key={feature.id} className="mx_UserSettings_toggle">
<input
type="checkbox"
id={feature.id}
name={feature.id}
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
onChange={ onChange }
/>
<label htmlFor={feature.id}>{feature.name}</label>
</div>
);
});
return (
<div>
<h3>{ _t("Labs") }</h3>
@ -808,9 +839,6 @@ module.exports = React.createClass({
},
_renderDeactivateAccount: function() {
// We can't deactivate a guest account.
if (MatrixClientPeg.get().isGuest()) return null;
return <div>
<h3>{ _t("Deactivate Account") }</h3>
<div className="mx_UserSettings_section">
@ -847,9 +875,10 @@ module.exports = React.createClass({
if (!this.state.rejectingInvites) {
// bind() the invited rooms so any new invites that may come in as this button is clicked
// don't inadvertently get rejected as well.
const onClick = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
reject = (
<AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onRejectAllInvitesClicked.bind(this, invitedRooms)}>
onClick={onClick}>
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
</AccessibleButton>
);
@ -867,8 +896,6 @@ module.exports = React.createClass({
const settings = this.state.electron_settings;
if (!settings) return;
const {ipcRenderer} = require('electron');
return <div>
<h3>{ _t('Desktop specific') }</h3>
<div className="mx_UserSettings_section">
@ -876,9 +903,7 @@ module.exports = React.createClass({
<input type="checkbox"
name="auto-launch"
defaultChecked={settings['auto-launch']}
onChange={(e) => {
ipcRenderer.send('settings_set', 'auto-launch', e.target.checked);
}}
onChange={this._onAutoLaunchChanged}
/>
<label htmlFor="auto-launch">{_t('Start automatically after system login')}</label>
</div>
@ -886,6 +911,115 @@ module.exports = React.createClass({
</div>;
},
_onAutoLaunchChanged: function(e) {
const {ipcRenderer} = require('electron');
ipcRenderer.send('settings_set', 'auto-launch', e.target.checked);
},
_mapWebRtcDevicesToSpans: function(devices) {
return devices.map((device) => <span key={device.deviceId}>{device.label}</span>);
},
_setAudioInput: function(deviceId) {
this.setState({activeAudioInput: deviceId});
CallMediaHandler.setAudioInput(deviceId);
},
_setVideoInput: function(deviceId) {
this.setState({activeVideoInput: deviceId});
CallMediaHandler.setVideoInput(deviceId);
},
_requestMediaPermissions: function(event) {
const getUserMedia = (
window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia
);
if (getUserMedia) {
return getUserMedia.apply(window.navigator, [
{ video: true, audio: true },
this._refreshMediaDevices,
function() {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createDialog(ErrorDialog, {
title: _t('No media permissions'),
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
});
},
]);
}
},
_renderWebRtcSettings: function() {
if (this.state.mediaDevices === false) {
return <div>
<h3>{_t('VoIP')}</h3>
<div className="mx_UserSettings_section">
<p className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
{_t('Missing Media Permissions, click here to request.')}
</p>
</div>
</div>;
} 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>;
const defaultOption = {
deviceId: '',
label: _t('Default Device'),
};
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
if (audioInputs.length > 0) {
let defaultInput = '';
if (!audioInputs.some((input) => input.deviceId === 'default')) {
audioInputs.unshift(defaultOption);
} else {
defaultInput = 'default';
}
microphoneDropdown = <div>
<h4>{_t('Microphone')}</h4>
<Dropdown
className="mx_UserSettings_webRtcDevices_dropdown"
value={this.state.activeAudioInput || defaultInput}
onOptionChange={this._setAudioInput}>
{this._mapWebRtcDevicesToSpans(audioInputs)}
</Dropdown>
</div>;
}
const videoInputs = this.state.mediaDevices.videoinput.slice(0);
if (videoInputs.length > 0) {
let defaultInput = '';
if (!videoInputs.some((input) => input.deviceId === 'default')) {
videoInputs.unshift(defaultOption);
} else {
defaultInput = 'default';
}
webcamDropdown = <div>
<h4>{_t('Camera')}</h4>
<Dropdown
className="mx_UserSettings_webRtcDevices_dropdown"
value={this.state.activeVideoInput || defaultInput}
onOptionChange={this._setVideoInput}>
{this._mapWebRtcDevicesToSpans(videoInputs)}
</Dropdown>
</div>;
}
return <div>
<h3>{_t('VoIP')}</h3>
<div className="mx_UserSettings_section">
{microphoneDropdown}
{webcamDropdown}
</div>
</div>;
},
_showSpoiler: function(event) {
const target = event.target;
target.innerHTML = target.getAttribute('data-spoiler');
@ -938,6 +1072,9 @@ module.exports = React.createClass({
const threepidsSection = this.state.threepids.map((val, pidIndex) => {
const id = "3pid-" + val.address;
// TODO: make a separate component to avoid having to rebind onClick
// each time we render
const onRemoveClick = (e) => this.onRemoveThreepidClicked(val);
return (
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
<div className="mx_UserSettings_profileLabelCell">
@ -949,7 +1086,8 @@ 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") } onClick={this.onRemoveThreepidClicked.bind(this, val)} />
<img src="img/cancel-small.svg" width="14" height="14" alt={ _t("Remove") }
onClick={onRemoveClick} />
</div>
</div>
);
@ -957,7 +1095,7 @@ module.exports = React.createClass({
let addEmailSection;
if (this.state.email_add_pending) {
addEmailSection = <Loader key="_email_add_spinner" />;
} else if (!MatrixClientPeg.get().isGuest()) {
} else {
addEmailSection = (
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
<div className="mx_UserSettings_profileLabelCell">
@ -973,7 +1111,7 @@ module.exports = React.createClass({
onValueChanged={ this._onAddEmailEditFinished } />
</div>
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={this._addEmail} />
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
</div>
</div>
);
@ -985,16 +1123,7 @@ module.exports = React.createClass({
threepidsSection.push(addEmailSection);
threepidsSection.push(addMsisdnSection);
let accountJsx;
if (MatrixClientPeg.get().isGuest()) {
accountJsx = (
<div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}>
{ _t("Create an account") }
</div>
);
} else {
accountJsx = (
const accountJsx = (
<ChangePassword
className="mx_UserSettings_accountTable"
rowClassName="mx_UserSettings_profileTableRow"
@ -1003,10 +1132,10 @@ module.exports = React.createClass({
buttonClassName="mx_UserSettings_button mx_UserSettings_changePasswordButton"
onError={this.onPasswordChangeError}
onFinished={this.onPasswordChanged} />
);
}
);
let notificationArea;
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
if (this.state.threepids !== undefined) {
notificationArea = (<div>
<h3>{ _t("Notifications") }</h3>
@ -1083,6 +1212,7 @@ module.exports = React.createClass({
{this._renderUserInterfaceSettings()}
{this._renderLabs()}
{this._renderWebRtcSettings()}
{this._renderDevicesPanel()}
{this._renderCryptoInfo()}
{this._renderBulkOptions()}
@ -1099,7 +1229,12 @@ module.exports = React.createClass({
{ _t("Logged in as:") } {this._me}
</div>
<div className="mx_UserSettings_advanced">
{_t('Access Token:')} <span className="mx_UserSettings_advanced_spoiler" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }>&lt;{ _t("click to reveal") }&gt;</span>
{_t('Access Token:')}
<span className="mx_UserSettings_advanced_spoiler"
onClick={this._showSpoiler}
data-spoiler={ MatrixClientPeg.get().getAccessToken() }>
&lt;{ _t("click to reveal") }&gt;
</span>
</div>
<div className="mx_UserSettings_advanced">
{ _t("Homeserver is") } { MatrixClientPeg.get().getHomeserverUrl() }

View File

@ -229,7 +229,7 @@ module.exports = React.createClass({
<div className="mx_Login_error">
</div>
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
Return to login
{_t('Return to login screen')}
</a>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
{ _t('Create an account') }

View File

@ -18,8 +18,7 @@ limitations under the License.
'use strict';
import React from 'react';
import { _t } from '../../../languageHandler';
import ReactDOM from 'react-dom';
import { _t, _tJsx } from '../../../languageHandler';
import sdk from '../../../index';
import Login from '../../../Login';
@ -88,7 +87,27 @@ module.exports = React.createClass({
).then((data) => {
this.props.onLoggedIn(data);
}, (error) => {
this._setStateFromError(error, true);
let errorText;
// Some error strings only apply for logging in
const usingEmail = username.indexOf("@") > 0;
if (error.httpStatus == 400 && usingEmail) {
errorText = _t('This Home Server does not support login using email address.');
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
errorText = _t('Incorrect username and/or password.');
} else {
// other errors, not specific to doing a password login
errorText = this._errorTextFromError(error);
}
this.setState({
errorText: errorText,
// 401 would be the sensible status code for 'incorrect password'
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
// mentions this (although the bug is for UI auth which is not this)
// We treat both as an incorrect password
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
});
}).finally(() => {
this.setState({
busy: false
@ -111,7 +130,16 @@ module.exports = React.createClass({
this._loginLogic.loginAsGuest().then(function(data) {
self.props.onLoggedIn(data);
}, function(error) {
self._setStateFromError(error, true);
let errorText;
if (error.httpStatus === 403) {
errorText = _t("Guest access is disabled on this Home Server.");
} else {
errorText = self._errorTextFromError(error);
}
self.setState({
errorText: errorText,
loginIncorrect: false,
});
}).finally(function() {
self.setState({
busy: false
@ -130,7 +158,7 @@ module.exports = React.createClass({
onPhoneNumberChanged: function(phoneNumber) {
// Validate the phone number entered
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
this.setState({ errorText: 'The phone number entered looks invalid' });
this.setState({ errorText: _t('The phone number entered looks invalid') });
return;
}
@ -184,55 +212,48 @@ module.exports = React.createClass({
currentFlow: self._getCurrentFlowStep(),
});
}, function(err) {
self._setStateFromError(err, false);
self.setState({
errorText: self._errorTextFromError(err),
loginIncorrect: false,
});
}).finally(function() {
self.setState({
busy: false,
});
});
}).done();
},
_getCurrentFlowStep: function() {
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
},
_setStateFromError: function(err, isLoginAttempt) {
this.setState({
errorText: this._errorTextFromError(err),
// https://matrix.org/jira/browse/SYN-744
loginIncorrect: isLoginAttempt && (err.httpStatus == 401 || err.httpStatus == 403)
});
},
_errorTextFromError(err) {
if (err.friendlyText) {
return err.friendlyText;
}
let errCode = err.errcode;
if (!errCode && err.httpStatus) {
errCode = "HTTP " + err.httpStatus;
}
let errorText = "Error: Problem communicating with the given homeserver " +
(errCode ? "(" + errCode + ")" : "");
let errorText = _t("Error: Problem communicating with the given homeserver.") +
(errCode ? " (" + errCode + ")" : "");
if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' &&
(this.state.enteredHomeserverUrl.startsWith("http:") ||
!this.state.enteredHomeserverUrl.startsWith("http")))
{
const urlStart = <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>;
const urlEnd = </a>;
!this.state.enteredHomeserverUrl.startsWith("http"))
) {
errorText = <span>
{ _t('Can\'t connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or %(urlStart)s enable unsafe scripts %(urlEnd)s', {urlStart: urlStart, urlEnd: urlEnd})}
{ _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
"Either use HTTPS or <a>enable unsafe scripts</a>.",
/<a>(.*?)<\/a>/,
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }
)}
</span>;
}
else {
const urlStart = <a href={this.state.enteredHomeserverUrl}>;
const urlEnd = </a>;
} else {
errorText = <span>
{ _t('Can\'t connect to homeserver - please check your connectivity and ensure your %(urlStart)s homeserver\'s SSL certificate %(urlEnd)s is trusted', {urlStart: urlStart, urlEnd: urlEnd})}
{ _tJsx("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
/<a>(.*?)<\/a>/,
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }
)}
</span>;
}
}

View File

@ -50,7 +50,7 @@ module.exports = React.createClass({
});
}, function(error) {
self.setState({
errorString: "Failed to fetch avatar URL",
errorString: _t("Failed to fetch avatar URL"),
busy: false
});
});

View File

@ -21,11 +21,9 @@ import q from 'q';
import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import ServerConfig from '../../views/login/ServerConfig';
import MatrixClientPeg from '../../../MatrixClientPeg';
import RegistrationForm from '../../views/login/RegistrationForm';
import CaptchaForm from '../../views/login/CaptchaForm';
import RtsClient from '../../../RtsClient';
import { _t } from '../../../languageHandler';
@ -47,8 +45,6 @@ module.exports = React.createClass({
brand: React.PropTypes.string,
email: React.PropTypes.string,
referrer: React.PropTypes.string,
username: React.PropTypes.string,
guestAccessToken: React.PropTypes.string,
teamServerConfig: React.PropTypes.shape({
// Email address to request new teams
supportEmail: React.PropTypes.string.isRequired,
@ -99,7 +95,7 @@ module.exports = React.createClass({
this.props.teamServerConfig.teamServerURL &&
!this._rtsClient
) {
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL);
this._rtsClient = this.props.rtsClient || new RtsClient(this.props.teamServerConfig.teamServerURL);
this.setState({
teamServerBusy: true,
@ -222,7 +218,6 @@ module.exports = React.createClass({
}
trackPromise.then((teamToken) => {
console.info('Team token promise',teamToken);
this.props.onLoggedIn({
userId: response.user_id,
deviceId: response.device_id,
@ -267,7 +262,7 @@ module.exports = React.createClass({
errMsg = _t('Passwords don\'t match.');
break;
case "RegistrationForm.ERR_PASSWORD_LENGTH":
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH: $MIN_PASSWORD_LENGTH})
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH: MIN_PASSWORD_LENGTH});
break;
case "RegistrationForm.ERR_EMAIL_INVALID":
errMsg = _t('This doesn\'t look like a valid email address.');
@ -298,17 +293,6 @@ module.exports = React.createClass({
},
_makeRegisterRequest: function(auth) {
let guestAccessToken = this.props.guestAccessToken;
if (
this.state.formVals.username !== this.props.username ||
this.state.hsUrl != this.props.defaultHsUrl
) {
// don't try to upgrade if we changed our username
// or are registering on a different HS
guestAccessToken = null;
}
// Only send the bind params if we're sending username / pw params
// (Since we need to send no params at all to use the ones saved in the
// session).
@ -323,7 +307,7 @@ module.exports = React.createClass({
undefined, // session id: included in the auth dict already
auth,
bindThreepids,
guestAccessToken,
null,
);
},
@ -360,10 +344,6 @@ module.exports = React.createClass({
} else if (this.state.busy || this.state.teamServerBusy) {
registerBody = <Spinner />;
} else {
let guestUsername = this.props.username;
if (this.state.hsUrl != this.props.defaultHsUrl) {
guestUsername = null;
}
let errorSection;
if (this.state.errorText) {
errorSection = <div className="mx_Login_error">{this.state.errorText}</div>;
@ -377,7 +357,6 @@ module.exports = React.createClass({
defaultPhoneNumber={this.state.formVals.phoneNumber}
defaultPassword={this.state.formVals.password}
teamsConfig={this.state.teamsConfig}
guestUsername={guestUsername}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit}

View File

@ -32,6 +32,7 @@ module.exports = React.createClass({
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
width: React.PropTypes.number,
height: React.PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: React.PropTypes.string,
defaultToInitialLetter: React.PropTypes.bool // true to add default url
},

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
import { _t } from '../../../languageHandler';
var Presets = {
PrivateChat: "private_chat",
@ -46,9 +47,9 @@ module.exports = React.createClass({
render: function() {
return (
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
<option value={this.Presets.PrivateChat}>Private Chat</option>
<option value={this.Presets.PublicChat}>Public Chat</option>
<option value={this.Presets.Custom}>Custom</option>
<option value={this.Presets.PrivateChat}>{_t("Private Chat")}</option>
<option value={this.Presets.PublicChat}>{_t("Public Chat")}</option>
<option value={this.Presets.Custom}>{_t("Custom")}</option>
</select>
);
}

View File

@ -15,6 +15,7 @@ limitations under the License.
*/
var React = require('react');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'RoomAlias',
@ -94,7 +95,7 @@ module.exports = React.createClass({
render: function() {
return (
<input type="text" className="mx_RoomAlias" placeholder="Alias (optional)"
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias}/>
);

View File

@ -16,36 +16,30 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton';
import Unread from '../../../Unread';
import classNames from 'classnames';
import createRoom from '../../../createRoom';
export default class ChatCreateOrReuseDialog extends React.Component {
constructor(props) {
super(props);
this.onNewDMClick = this.onNewDMClick.bind(this);
this.onRoomTileClick = this.onRoomTileClick.bind(this);
this.state = {
tiles: [],
profile: {
displayName: null,
avatarUrl: null,
},
profileError: null,
};
}
onNewDMClick() {
createRoom({dmUserId: this.props.userId});
this.props.onFinished(true);
}
onRoomTileClick(roomId) {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
this.props.onFinished(true);
}
render() {
componentWillMount() {
const client = MatrixClientPeg.get();
const dmRoomMap = new DMRoomMap(client);
@ -70,40 +64,123 @@ export default class ChatCreateOrReuseDialog extends React.Component {
highlight={highlight}
isInvite={me.membership == "invite"}
onClick={this.onRoomTileClick}
/>
/>,
);
}
}
const labelClasses = classNames({
mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true,
this.setState({
tiles: tiles,
});
const startNewChat = <AccessibleButton
className="mx_MemberInfo_createRoom"
onClick={this.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>{_("Start new chat")}</i></div>
</AccessibleButton>;
if (tiles.length === 0) {
this.setState({
busyProfile: true,
});
MatrixClientPeg.get().getProfileInfo(this.props.userId).done((resp) => {
const profile = {
displayName: resp.displayname,
avatarUrl: null,
};
if (resp.avatar_url) {
profile.avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(
resp.avatar_url, 48, 48, "crop",
);
}
this.setState({
busyProfile: false,
profile: profile,
});
}, (err) => {
console.error(
'Unable to get profile for user ' + this.props.userId + ':',
err,
);
this.setState({
busyProfile: false,
profileError: err,
});
});
}
}
onRoomTileClick(roomId) {
this.props.onExistingRoomSelected(roomId);
}
render() {
let title = '';
let content = null;
if (this.state.tiles.length > 0) {
// Show the existing rooms with a "+" to add a new dm
title = _t('Create a new chat or reuse an existing one');
const labelClasses = classNames({
mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true,
});
const startNewChat = <AccessibleButton
className="mx_MemberInfo_createRoom"
onClick={this.props.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
</AccessibleButton>;
content = <div className="mx_Dialog_content">
{ _t('You already have existing direct chats with this user:') }
<div className="mx_ChatCreateOrReuseDialog_tiles">
{ this.state.tiles }
{ startNewChat }
</div>
</div>;
} else {
// Show the avatar, name and a button to confirm that a new chat is requested
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const Spinner = sdk.getComponent('elements.Spinner');
title = _t('Start chatting');
let profile = null;
if (this.state.busyProfile) {
profile = <Spinner />;
} else if (this.state.profileError) {
profile = <div className="error">
Unable to load profile information for { this.props.userId }
</div>;
} else {
profile = <div className="mx_ChatCreateOrReuseDialog_profile">
<BaseAvatar
name={this.state.profile.displayName || this.props.userId}
url={this.state.profile.avatarUrl}
width={48} height={48}
/>
<div className="mx_ChatCreateOrReuseDialog_profile_name">
{this.state.profile.displayName || this.props.userId}
</div>
</div>;
}
content = <div>
<div className="mx_Dialog_content">
<p>
{ _t('Click on the button below to start chatting!') }
</p>
{ profile }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
{ _t('Start Chatting') }
</button>
</div>
</div>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={() => {
this.props.onFinished(false)
}}
title='Create a new chat or reuse an existing one'
onFinished={ this.props.onFinished.bind(false) }
title={title}
>
<div className="mx_Dialog_content">
You already have existing direct chats with this user:
<div className="mx_ChatCreateOrReuseDialog_tiles">
{tiles}
{startNewChat}
</div>
</div>
{ content }
</BaseDialog>
);
}
@ -111,5 +188,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
ChatCreateOrReuseDialog.propTyps = {
userId: React.PropTypes.string.isRequired,
// Called when clicking outside of the dialog
onFinished: React.PropTypes.func.isRequired,
onNewDMClick: React.PropTypes.func.isRequired,
onExistingRoomSelected: React.PropTypes.func.isRequired,
};

View File

@ -15,20 +15,18 @@ limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
import createRoom from '../../../createRoom';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import rate_limited_func from '../../../ratelimitedfunc';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import q from 'q';
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
module.exports = React.createClass({
displayName: "ChatInviteDialog",
@ -43,13 +41,13 @@ module.exports = React.createClass({
roomId: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired
onFinished: React.PropTypes.func.isRequired,
},
getDefaultProps: function() {
return {
value: "",
focus: true
focus: true,
};
},
@ -57,12 +55,20 @@ module.exports = React.createClass({
return {
error: false,
// List of AddressTile.InviteAddressType objects represeting
// List of AddressTile.InviteAddressType objects representing
// the list of addresses we're going to invite
inviteList: [],
// List of AddressTile.InviteAddressType objects represeting
// the set of autocompletion results for the current search
// Whether a search is ongoing
busy: false,
// An error message generated during the user directory search
searchError: null,
// Whether the server supports the user_directory API
serverSupportsUserDirectory: true,
// The query being searched for
query: "",
// List of AddressTile.InviteAddressType objects representing
// the set of auto-completion results for the current search
// query.
queryList: [],
};
@ -73,7 +79,6 @@ module.exports = React.createClass({
// Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value;
}
this._updateUserList();
},
onButtonClick: function() {
@ -95,16 +100,25 @@ module.exports = React.createClass({
// A Direct Message room already exists for this user, so select a
// room from a list that is similar to the one in MemberInfo panel
const ChatCreateOrReuseDialog = sdk.getComponent(
"views.dialogs.ChatCreateOrReuseDialog"
"views.dialogs.ChatCreateOrReuseDialog",
);
Modal.createDialog(ChatCreateOrReuseDialog, {
userId: userId,
onFinished: (success) => {
if (success) {
this.props.onFinished(true, inviteList[0]);
}
// else show this ChatInviteDialog again
}
this.props.onFinished(success);
},
onNewDMClick: () => {
dis.dispatch({
action: 'start_chat',
user_id: userId,
});
},
onExistingRoomSelected: (roomId) => {
dis.dispatch({
action: 'view_room',
user_id: roomId,
});
},
});
} else {
this._startChat(inviteList);
@ -131,15 +145,15 @@ module.exports = React.createClass({
} else if (e.keyCode === 38) { // up arrow
e.stopPropagation();
e.preventDefault();
this.addressSelector.moveSelectionUp();
if (this.addressSelector) this.addressSelector.moveSelectionUp();
} else if (e.keyCode === 40) { // down arrow
e.stopPropagation();
e.preventDefault();
this.addressSelector.moveSelectionDown();
if (this.addressSelector) this.addressSelector.moveSelectionDown();
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
e.stopPropagation();
e.preventDefault();
this.addressSelector.chooseSelection();
if (this.addressSelector) this.addressSelector.chooseSelection();
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
e.stopPropagation();
e.preventDefault();
@ -162,74 +176,36 @@ module.exports = React.createClass({
onQueryChanged: function(ev) {
const query = ev.target.value.toLowerCase();
let queryList = [];
if (query.length < 2) {
return;
}
if (this.queryChangedDebouncer) {
clearTimeout(this.queryChangedDebouncer);
}
this.queryChangedDebouncer = setTimeout(() => {
// Only do search if there is something to search
if (query.length > 0 && query != '@') {
this._userList.forEach((user) => {
if (user.userId.toLowerCase().indexOf(query) === -1 &&
user.displayName.toLowerCase().indexOf(query) === -1
) {
return;
}
// Return objects, structure of which is defined
// by InviteAddressType
queryList.push({
addressType: 'mx',
address: user.userId,
displayName: user.displayName,
avatarMxc: user.avatarUrl,
isKnown: true,
order: user.getLastActiveTs(),
});
});
queryList = queryList.sort((a,b) => {
return a.order < b.order;
});
// If the query is a valid address, add an entry for that
// This is important, otherwise there's no way to invite
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (addrType !== null) {
queryList.unshift({
addressType: addrType,
address: query,
isKnown: false,
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
}
// 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);
} else {
this._doLocalSearch(query);
}
}
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
} else {
this.setState({
queryList: queryList,
error: false,
}, () => {
this.addressSelector.moveSelectionTop();
queryList: [],
query: "",
searchError: null,
});
}, 200);
}
},
onDismissed: function(index) {
var self = this;
return function() {
return () => {
var inviteList = self.state.inviteList.slice();
inviteList.splice(index, 1);
self.setState({
inviteList: inviteList,
queryList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
};
@ -248,10 +224,103 @@ module.exports = React.createClass({
this.setState({
inviteList: inviteList,
queryList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
},
_doUserDirectorySearch: function(query) {
this.setState({
busy: true,
query,
searchError: null,
});
MatrixClientPeg.get().searchUserDirectory({
term: query,
}).then((resp) => {
this._processResults(resp.results, query);
}).catch((err) => {
console.error('Error whilst searching user directory: ', err);
this.setState({
searchError: err.errcode ? err.message : _t('Something went wrong!'),
});
if (err.errcode === 'M_UNRECOGNIZED') {
this.setState({
serverSupportsUserDirectory: false,
});
// Do a local search immediately
this._doLocalSearch(query);
}
}).done(() => {
this.setState({
busy: false,
});
});
},
_doLocalSearch: function(query) {
this.setState({
query,
searchError: null,
});
const results = [];
MatrixClientPeg.get().getUsers().forEach((user) => {
if (user.userId.toLowerCase().indexOf(query) === -1 &&
user.displayName.toLowerCase().indexOf(query) === -1
) {
return;
}
// Put results in the format of the new API
results.push({
user_id: user.userId,
display_name: user.displayName,
avatar_url: user.avatarUrl,
});
});
this._processResults(results, query);
},
_processResults: function(results, query) {
const queryList = [];
results.forEach((user) => {
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
return;
}
// Return objects, structure of which is defined
// by InviteAddressType
queryList.push({
addressType: 'mx',
address: user.user_id,
displayName: user.display_name,
avatarMxc: user.avatar_url,
isKnown: true,
});
});
// If the query is a valid address, add an entry for that
// This is important, otherwise there's no way to invite
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (addrType !== null) {
queryList.unshift({
addressType: addrType,
address: query,
isKnown: false,
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
}
}
this.setState({
queryList,
error: false,
}, () => {
if (this.addressSelector) this.addressSelector.moveSelectionTop();
});
},
_getDirectMessageRooms: function(addr) {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
@ -270,11 +339,7 @@ module.exports = React.createClass({
_startChat: function(addrs) {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: _t("Please Register"),
description: _t("Guest users can't invite users. Please register."),
});
dis.dispatch({action: 'view_set_mxid'});
return;
}
@ -340,16 +405,6 @@ module.exports = React.createClass({
this.props.onFinished(true, addrTexts);
},
_updateUserList: function() {
// Get all the users
this._userList = MatrixClientPeg.get().getUsers();
// Remove current user
const meIx = this._userList.findIndex((u) => {
return u.userId === MatrixClientPeg.get().credentials.userId;
});
this._userList.splice(meIx, 1);
},
_isOnInviteList: function(uid) {
for (let i = 0; i < this.state.inviteList.length; i++) {
if (
@ -417,6 +472,7 @@ module.exports = React.createClass({
this.setState({
inviteList: inviteList,
queryList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return inviteList;
@ -452,7 +508,7 @@ module.exports = React.createClass({
displayName: res.displayname,
avatarMxc: res.avatar_url,
isKnown: true,
}]
}],
});
});
},
@ -484,23 +540,27 @@ module.exports = React.createClass({
placeholder={this.props.placeholder}
defaultValue={this.props.value}
autoFocus={this.props.focus}>
</textarea>
</textarea>,
);
var error;
var addressSelector;
let error;
let addressSelector;
if (this.state.error) {
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>;
error = <div className="mx_ChatInviteDialog_error">{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}</div>;
} else if (this.state.searchError) {
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>;
} else {
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
Searching known users
</div>;
addressSelector = (
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
addressList={ this.state.queryList }
onSelected={ this.onSelected }
truncateAt={ TRUNCATE_QUERY_LIST }
header={ addressSelectorHeader }
/>
);
}

View File

@ -86,7 +86,7 @@ export default class DeactivateAccountDialog extends React.Component {
passwordBoxClass = 'error';
}
const okLabel = this.state.busy ? <Loader /> : 'Deactivate Account';
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
const okEnabled = this.state.confirmButtonEnabled && !this.state.busy;
let cancelButton = null;

View File

@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Matrix from 'matrix-js-sdk';
import React from 'react';
import sdk from '../../../index';
@ -80,7 +78,7 @@ export default React.createClass({
<AccessibleButton onClick={this._onDismissClick}
className="mx_UserSettings_button"
>
Dismiss
{_t("Dismiss")}
</AccessibleButton>
</div>
);

View File

@ -1,72 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* Usage:
* Modal.createDialog(NeedToRegisterDialog, {
* title: "some text", (default: "Registration required")
* description: "some more text",
* onFinished: someFunction,
* });
*/
import React from 'react';
import dis from '../../../dispatcher';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'NeedToRegisterDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.string,
]),
onFinished: React.PropTypes.func.isRequired,
},
onRegisterClicked: function() {
dis.dispatch({
action: "start_upgrade_registration",
});
if (this.props.onFinished) {
this.props.onFinished();
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_NeedToRegisterDialog"
onFinished={this.props.onFinished}
title={this.props.title || _t('Registration required')}
>
<div className="mx_Dialog_content">
{this.props.description || _t('A registered account is required for this action')}
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
Cancel
</button>
<button onClick={this.onRegisterClicked}>
Register
</button>
</div>
</BaseDialog>
);
},
});

View File

@ -51,7 +51,7 @@ export default React.createClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? (
<button onClick={this.onCancel}>
Cancel
{_t("Cancel")}
</button>
) : null;
return (

View File

@ -18,7 +18,7 @@ import React from 'react';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import { _t, _tJsx } from '../../../languageHandler';
export default React.createClass({
@ -44,8 +44,11 @@ export default React.createClass({
if (SdkConfig.get().bug_report_endpoint_url) {
bugreport = (
<p>Otherwise, <a onClick={this._sendBugReport} href='#'>
click here</a> to send a bug report.
<p>
{_tJsx(
"Otherwise, <a>click here</a> to send a bug report.",
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{sub}</a>,
)}
</p>
);
}

View File

@ -1,87 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
/**
* Prompt the user to set a display name.
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default React.createClass({
displayName: 'SetDisplayNameDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
currentDisplayName: React.PropTypes.string,
},
getInitialState: function() {
if (this.props.currentDisplayName) {
return { value: this.props.currentDisplayName };
}
if (MatrixClientPeg.get().isGuest()) {
return { value : "Guest " + MatrixClientPeg.get().getUserIdLocalpart() };
}
else {
return { value : MatrixClientPeg.get().getUserIdLocalpart() };
}
},
componentDidMount: function() {
this.refs.input_value.select();
},
onValueChange: function(ev) {
this.setState({
value: ev.target.value
});
},
onFormSubmit: function(ev) {
ev.preventDefault();
this.props.onFinished(true, this.state.value);
return false;
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_SetDisplayNameDialog"
onFinished={this.props.onFinished}
title={_t("Set a Display Name")}
>
<div className="mx_Dialog_content">
{_t("Your display name is how you'll appear to others when you speak in rooms. " +
"What would you like it to be?")}
</div>
<form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content">
<input type="text" ref="input_value" value={this.state.value}
autoFocus={true} onChange={this.onValueChange} size="30"
className="mx_SetDisplayNameDialog_input"
/>
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary" type="submit" value="Set" />
</div>
</form>
</BaseDialog>
);
},
});

View File

@ -0,0 +1,294 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import q from 'q';
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
import KeyCode from '../../../KeyCode';
import { _t, _tJsx } from '../../../languageHandler';
// The amount of time to wait for further changes to the input username before
// sending a request to the server
const USERNAME_CHECK_DEBOUNCE_MS = 250;
/**
* Prompt the user to set a display name.
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default React.createClass({
displayName: 'SetMxIdDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
// Called when the user requests to register with a different homeserver
onDifferentServerClicked: React.PropTypes.func.isRequired,
// Called if the user wants to switch to login instead
onLoginClick: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
// The entered username
username: '',
// Indicate ongoing work on the username
usernameBusy: false,
// Indicate error with username
usernameError: '',
// Assume the homeserver supports username checking until "M_UNRECOGNIZED"
usernameCheckSupport: true,
// Whether the auth UI is currently being used
doingUIAuth: false,
// Indicate error with auth
authError: '',
};
},
componentDidMount: function() {
this.refs.input_value.select();
this._matrixClient = MatrixClientPeg.get();
},
onValueChange: function(ev) {
this.setState({
username: ev.target.value,
usernameBusy: true,
usernameError: '',
}, () => {
if (!this.state.username || !this.state.usernameCheckSupport) {
this.setState({
usernameBusy: false,
});
return;
}
// Debounce the username check to limit number of requests sent
if (this._usernameCheckTimeout) {
clearTimeout(this._usernameCheckTimeout);
}
this._usernameCheckTimeout = setTimeout(() => {
this._doUsernameCheck().finally(() => {
this.setState({
usernameBusy: false,
});
});
}, USERNAME_CHECK_DEBOUNCE_MS);
});
},
onKeyUp: function(ev) {
if (ev.keyCode === KeyCode.ENTER) {
this.onSubmit();
}
},
onSubmit: function(ev) {
this.setState({
doingUIAuth: true,
});
},
_doUsernameCheck: function() {
// Check if username is available
return this._matrixClient.isUsernameAvailable(this.state.username).then(
(isAvailable) => {
if (isAvailable) {
this.setState({usernameError: ''});
}
},
(err) => {
// Indicate whether the homeserver supports username checking
const newState = {
usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED",
};
console.error('Error whilst checking username availability: ', err);
switch (err.errcode) {
case "M_USER_IN_USE":
newState.usernameError = _t('Username not available');
break;
case "M_INVALID_USERNAME":
newState.usernameError = _t(
'Username invalid: %(errMessage)s',
{ errMessage: err.message},
);
break;
case "M_UNRECOGNIZED":
// This homeserver doesn't support username checking, assume it's
// fine and rely on the error appearing in registration step.
newState.usernameError = '';
break;
case undefined:
newState.usernameError = _t('Something went wrong!');
break;
default:
newState.usernameError = _t(
'An error occurred: %(error_string)s',
{ error_string: err.message },
);
break;
}
this.setState(newState);
},
);
},
_generatePassword: function() {
return Math.random().toString(36).slice(2);
},
_makeRegisterRequest: function(auth) {
// Not upgrading - changing mxids
const guestAccessToken = null;
if (!this._generatedPassword) {
this._generatedPassword = this._generatePassword();
}
return this._matrixClient.register(
this.state.username,
this._generatedPassword,
undefined, // session id: included in the auth dict already
auth,
{},
guestAccessToken,
);
},
_onUIAuthFinished: function(success, response) {
this.setState({
doingUIAuth: false,
});
if (!success) {
this.setState({ authError: response.message });
return;
}
// XXX Implement RTS /register here
const teamToken = null;
this.props.onFinished(true, {
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token,
password: this._generatedPassword,
teamToken: teamToken,
});
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent('elements.Spinner');
let auth;
if (this.state.doingUIAuth) {
auth = <InteractiveAuth
matrixClient={this._matrixClient}
makeRequest={this._makeRegisterRequest}
onAuthFinished={this._onUIAuthFinished}
inputs={{}}
poll={true}
/>;
}
const inputClasses = classnames({
"mx_SetMxIdDialog_input": true,
"error": Boolean(this.state.usernameError),
});
let usernameIndicator = null;
let usernameBusyIndicator = null;
if (this.state.usernameBusy) {
usernameBusyIndicator = <Spinner w="24" h="24"/>;
} else {
const usernameAvailable = this.state.username &&
this.state.usernameCheckSupport && !this.state.usernameError;
const usernameIndicatorClasses = classnames({
"error": Boolean(this.state.usernameError),
"success": usernameAvailable,
});
usernameIndicator = <div className={usernameIndicatorClasses}>
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
</div>;
}
let authErrorIndicator = null;
if (this.state.authError) {
authErrorIndicator = <div className="error">
{ this.state.authError }
</div>;
}
const canContinue = this.state.username &&
!this.state.usernameError &&
!this.state.usernameBusy;
return (
<BaseDialog className="mx_SetMxIdDialog"
onFinished={this.props.onFinished}
title="To get started, please pick a username!"
>
<div className="mx_Dialog_content">
<div className="mx_SetMxIdDialog_input_group">
<input type="text" ref="input_value" value={this.state.username}
autoFocus={true}
onChange={this.onValueChange}
onKeyUp={this.onKeyUp}
size="30"
className={inputClasses}
/>
{ usernameBusyIndicator }
</div>
{ usernameIndicator }
<p>
{ _tJsx(
'This will be your account name on the <span></span> ' +
'homeserver, or you can pick a <a>different server</a>.',
[
/<span><\/span>/,
/<a>(.*?)<\/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>],
)}
</p>
{ auth }
{ authErrorIndicator }
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary"
type="submit"
value={_t("Continue")}
onClick={this.onSubmit}
disabled={!canContinue}
/>
</div>
</BaseDialog>
);
},
});

View File

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend';
@ -146,7 +145,7 @@ export default React.createClass({
console.log("UnknownDeviceDialog closed by escape");
this.props.onFinished();
}}
title='Room contains unknown devices'
title={_t('Room contains unknown devices')}
>
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4>
@ -163,7 +162,7 @@ export default React.createClass({
this.props.onFinished();
Resend.resendUnsentEvents(this.props.room);
}}>
Send anyway
{_t("Send anyway")}
</button>
<button className="mx_Dialog_primary" autoFocus={ true }
onClick={() => {

View File

@ -0,0 +1,84 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher';
import sdk from '../../../index';
export default React.createClass({
displayName: 'RoleButton',
propTypes: {
size: PropTypes.string,
tooltip: PropTypes.bool,
action: PropTypes.string.isRequired,
mouseOverAction: PropTypes.string,
label: PropTypes.string.isRequired,
iconPath: PropTypes.string.isRequired,
},
getDefaultProps: function() {
return {
size: "25",
tooltip: false,
};
},
getInitialState: function() {
return {
showTooltip: false,
};
},
_onClick: function(ev) {
ev.stopPropagation();
dis.dispatch({action: this.props.action});
},
_onMouseEnter: function() {
if (this.props.tooltip) this.setState({showTooltip: true});
if (this.props.mouseOverAction) {
dis.dispatch({action: this.props.mouseOverAction});
}
},
_onMouseLeave: function() {
this.setState({showTooltip: false});
},
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let tooltip;
if (this.state.showTooltip) {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
}
return (
<AccessibleButton className="mx_RoleButton"
onClick={this._onClick}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}
>
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
{tooltip}
</AccessibleButton>
);
}
});

View File

@ -19,9 +19,7 @@ limitations under the License.
import React from 'react';
import classNames from 'classnames';
import sdk from "../../../index";
import Invite from "../../../Invite";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Avatar from '../../../Avatar';
import { _t } from '../../../languageHandler';
// React PropType definition for an object describing

View File

@ -0,0 +1,40 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const CreateRoomButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_create_room"
mouseOverAction={props.callout ? "callout_create_room" : null}
label={ _t("Create new room") }
iconPath="img/icons-create-room.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
CreateRoomButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default CreateRoomButton;

View File

@ -0,0 +1,39 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const HomeButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_home_page"
label={ _t("Home") }
iconPath="img/icons-home.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
HomeButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default HomeButton;

View File

@ -19,7 +19,6 @@ import React from 'react';
import sdk from '../../../index';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler';
function languageMatchesSearchQuery(query, language) {
@ -40,14 +39,7 @@ export default class LanguageDropdown extends React.Component {
}
componentWillMount() {
languageHandler.getAllLanguageKeysFromJson().then((langKeys) => {
const langs = [];
langKeys.forEach((languageKey) => {
langs.push({
value: languageKey,
label: _t(languageKey)
});
});
languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b){
if(a.label < b.label) return -1;
if(a.label > b.label) return 1;

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
const MemberAvatar = require('../avatars/MemberAvatar.js');
import { _t } from '../../../languageHandler';
@ -111,9 +112,13 @@ module.exports = React.createClass({
return null;
}
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
{summaries.join(", ")}
<EmojiText>
{summaries.join(", ")}
</EmojiText>
</span>
);
},
@ -222,8 +227,7 @@ module.exports = React.createClass({
? _t("%(severalUsers)sjoined", { severalUsers: "" })
: _t("%(oneUser)sjoined", { oneUser: "" });
}
break;
break;
case "left":
if (repeats > 1) {
res = (plural)
@ -233,7 +237,8 @@ module.exports = React.createClass({
res = (plural)
? _t("%(severalUsers)sleft", { severalUsers: "" })
: _t("%(oneUser)sleft", { oneUser: "" });
} break;
}
break;
case "joined_and_left":
if (repeats > 1) {
res = (plural)
@ -244,7 +249,7 @@ module.exports = React.createClass({
? _t("%(severalUsers)sjoined and left", { severalUsers: "" })
: _t("%(oneUser)sjoined and left", { oneUser: "" });
}
break;
break;
case "left_and_joined":
if (repeats > 1) {
res = (plural)
@ -254,8 +259,8 @@ module.exports = React.createClass({
res = (plural)
? _t("%(severalUsers)sleft and rejoined", { severalUsers: "" })
: _t("%(oneUser)sleft and rejoined", { oneUser: "" });
} break;
break;
}
break;
case "invite_reject":
if (repeats > 1) {
res = (plural)
@ -266,7 +271,7 @@ module.exports = React.createClass({
? _t("%(severalUsers)srejected their invitations", { severalUsers: "" })
: _t("%(oneUser)srejected their invitation", { oneUser: "" });
}
break;
break;
case "invite_withdrawal":
if (repeats > 1) {
res = (plural)
@ -277,7 +282,7 @@ module.exports = React.createClass({
? _t("%(severalUsers)shad their invitations withdrawn", { severalUsers: "" })
: _t("%(oneUser)shad their invitation withdrawn", { oneUser: "" });
}
break;
break;
case "invited":
if (repeats > 1) {
res = (plural)
@ -288,7 +293,7 @@ module.exports = React.createClass({
? _t("were invited")
: _t("was invited");
}
break;
break;
case "banned":
if (repeats > 1) {
res = (plural)
@ -299,7 +304,7 @@ module.exports = React.createClass({
? _t("were banned")
: _t("was banned");
}
break;
break;
case "unbanned":
if (repeats > 1) {
res = (plural)
@ -310,7 +315,7 @@ module.exports = React.createClass({
? _t("were unbanned")
: _t("was unbanned");
}
break;
break;
case "kicked":
if (repeats > 1) {
res = (plural)
@ -321,7 +326,7 @@ module.exports = React.createClass({
? _t("were kicked")
: _t("was kicked");
}
break;
break;
case "changed_name":
if (repeats > 1) {
res = (plural)
@ -332,7 +337,7 @@ module.exports = React.createClass({
? _t("%(severalUsers)schanged their name", { severalUsers: "" })
: _t("%(oneUser)schanged their name", { oneUser: "" });
}
break;
break;
case "changed_avatar":
if (repeats > 1) {
res = (plural)
@ -343,7 +348,7 @@ module.exports = React.createClass({
? _t("%(severalUsers)schanged their avatar", { severalUsers: "" })
: _t("%(oneUser)schanged their avatar", { oneUser: "" });
}
break;
break;
}
return res;

View File

@ -18,6 +18,7 @@ limitations under the License.
import React from 'react';
import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
var LEVEL_ROLE_MAP = {};
var reverseRoles = {};
@ -116,9 +117,9 @@ module.exports = React.createClass({
text: Roles.textualPowerLevel(level, 0),
}
});
options.push({ value: "Custom", text: "Custom level" });
options.push({ value: "Custom", text: _t("Custom level") });
options = options.map((op) => {
return <option value={op.value}>{op.text}</option>;
return <option value={op.value} key={op.value}>{op.text}</option>;
});
select =

View File

@ -0,0 +1,40 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const RoomDirectoryButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_room_directory"
mouseOverAction={props.callout ? "callout_room_directory" : null}
label={ _t("Room directory") }
iconPath="img/icons-directory.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
RoomDirectoryButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default RoomDirectoryButton;

View File

@ -0,0 +1,39 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const SettingsButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_user_settings"
label={ _t("Settings") }
iconPath="img/icons-settings.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
SettingsButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default SettingsButton;

View File

@ -0,0 +1,40 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const StartChatButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_create_chat"
mouseOverAction={props.callout ? "callout_start_chat" : null}
label={ _t("Start chat") }
iconPath="img/icons-people.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
StartChatButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default StartChatButton;

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'TruncatedList',
@ -33,7 +34,7 @@ module.exports = React.createClass({
truncateAt: 2,
createOverflowElement: function(overflowCount, totalCount) {
return (
<div>And {overflowCount} more...</div>
<div>{_t("And %(count)s more...", {count: overflowCount})}</div>
);
}
};

View File

@ -129,8 +129,8 @@ export const PasswordAuthEntry = React.createClass({
return (
<div>
<p>To continue, please enter your password.</p>
<p>Password:</p>
<p>{_t("To continue, please enter your password.")}</p>
<p>{_t("Password:")}</p>
<form onSubmit={this._onSubmit}>
<input
ref="passwordField"
@ -380,7 +380,7 @@ export const MsisdnAuthEntry = React.createClass({
onChange={this._onTokenChange}
/>
<br />
<input type="submit" value="Submit"
<input type="submit" value={_t("Submit")}
className={submitClasses}
disabled={!enableSubmit}
/>
@ -440,7 +440,7 @@ export const FallbackAuthEntry = React.createClass({
render: function() {
return (
<div>
<a onClick={this._onShowFallbackClick}>Start authentication</a>
<a onClick={this._onShowFallbackClick}>{_t("Start authentication")}</a>
<div className="error">
{this.props.errorText}
</div>

View File

@ -16,6 +16,7 @@ limitations under the License.
'use strict';
import { _t } from '../../../languageHandler';
import React from 'react';
module.exports = React.createClass({
@ -27,5 +28,5 @@ module.exports = React.createClass({
<a href="https://matrix.org">{_t("powered by Matrix")}</a>
</div>
);
}
},
});

View File

@ -16,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -150,7 +149,7 @@ class PasswordLogin extends React.Component {
type="text"
name="phoneNumber"
onChange={this.onPhoneNumberChanged}
placeholder="Mobile phone number"
placeholder={_t("Mobile phone number")}
value={this.state.phoneNumber}
autoFocus
/>
@ -182,7 +181,7 @@ class PasswordLogin extends React.Component {
<div>
<form onSubmit={this.onSubmitForm}>
<div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('I want to sign in with') }</label>
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Dropdown
className="mx_Login_type_dropdown"
value={this.state.loginType}

View File

@ -54,11 +54,6 @@ module.exports = React.createClass({
})).required,
}),
// A username that will be used if no username is entered.
// Specifying this param will also warn the user that entering
// a different username will cause a fresh account to be generated.
guestUsername: React.PropTypes.string,
minPasswordLength: React.PropTypes.number,
onError: React.PropTypes.func,
onRegisterClick: React.PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
@ -101,7 +96,7 @@ module.exports = React.createClass({
if (this.refs.email.value == '') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning!",
title: _t("Warning!"),
description:
<div>
{_t("If you don't specify an email address, you won't be able to reset your password. " +
@ -110,21 +105,20 @@ module.exports = React.createClass({
button: _t("Continue"),
onFinished: function(confirmed) {
if (confirmed) {
self._doSubmit();
self._doSubmit(ev);
}
},
});
}
else {
self._doSubmit();
} else {
self._doSubmit(ev);
}
}
},
_doSubmit: function() {
_doSubmit: function(ev) {
let email = this.refs.email.value.trim();
var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim() || this.props.guestUsername,
username: this.refs.username.value.trim(),
password: this.refs.password.value.trim(),
email: email,
phoneCountry: this.state.phoneCountry,
@ -192,7 +186,7 @@ module.exports = React.createClass({
break;
case FIELD_USERNAME:
// XXX: SPEC-1
var username = this.refs.username.value.trim() || this.props.guestUsername;
var username = this.refs.username.value.trim();
if (encodeURIComponent(username) != username) {
this.markFieldValid(
field_id,
@ -282,7 +276,7 @@ module.exports = React.createClass({
const emailSection = (
<div>
<input type="text" ref="email"
autoFocus={true} placeholder="Email address (optional)"
autoFocus={true} placeholder={_t("Email address (optional)")}
defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}}
@ -321,7 +315,7 @@ module.exports = React.createClass({
showPrefix={true}
/>
<input type="text" ref="phoneNumber"
placeholder="Mobile phone number (optional)"
placeholder={_t("Mobile phone number (optional)")}
defaultValue={this.props.defaultPhoneNumber}
className={this._classForField(
FIELD_PHONE_NUMBER,
@ -336,13 +330,10 @@ module.exports = React.createClass({
);
const registerButton = (
<input className="mx_Login_submit" type="submit" value="Register" />
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
);
let placeholderUserName = "User name";
if (this.props.guestUsername) {
placeholderUserName += " (default: " + this.props.guestUsername + ")";
}
let placeholderUserName = _t("User name");
return (
<div>
@ -355,16 +346,13 @@ module.exports = React.createClass({
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
<br />
{ this.props.guestUsername ?
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
}
<input type="password" ref="password"
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_PASSWORD);}}
placeholder="Password" defaultValue={this.props.defaultPassword} />
placeholder={_t("Password")} defaultValue={this.props.defaultPassword} />
<br />
<input type="password" ref="passwordConfirm"
placeholder="Confirm password"
placeholder={_t("Confirm password")}
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}}
defaultValue={this.props.defaultPassword} />

View File

@ -132,7 +132,7 @@ module.exports = React.createClass({
var toggleButton;
if (this.props.withToggleButton) {
toggleButton = (
<div style={{ textAlign: 'center' }}>
<div className="mx_ServerConfig_selector">
<input className="mx_Login_radio" id="basic" name="configVisible" type="radio"
checked={!this.state.configVisible}
onChange={this.onServerConfigVisibleChange.bind(this, false)} />

View File

@ -20,7 +20,6 @@ import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';

View File

@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
import {decryptFile} from '../../../utils/DecryptFile';
import Tinter from '../../../Tinter';
import request from 'browser-request';
import q from 'q';
import Modal from '../../../Modal';

View File

@ -19,8 +19,6 @@ limitations under the License.
import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Model from '../../../Modal';
import sdk from '../../../index';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import q from 'q';
import UserSettingsStore from '../../../UserSettingsStore';

View File

@ -0,0 +1,92 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk';
import { _t, _tJsx } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
displayName: 'RoomAvatarEvent',
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
},
onAvatarClick: function(name) {
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url);
var ImageView = sdk.getComponent("elements.ImageView");
var params = {
src: httpUrl,
name: name,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
},
render: function() {
var ev = this.props.mxEvent;
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
var name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
senderDisplayName: senderDisplayName,
roomName: room ? room.name : '',
});
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
return (
<div className="mx_TextualEvent">
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) }
</div>
);
}
var url = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
ev.getContent().url,
14 * window.devicePixelRatio,
14 * window.devicePixelRatio,
'crop'
);
// it sucks that _tJsx doesn't support normal _t substitutions :((
return (
<div className="mx_RoomAvatarEvent">
{ _tJsx('$senderDisplayName changed the room avatar to <img/>',
[
/\$senderDisplayName/,
/<img\/>/,
],
[
(sub) => senderDisplayName,
(sub) =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
onClick={ this.onAvatarClick.bind(this, name) }>
<BaseAvatar width={14} height={14} url={ url }
name={ name } />
</AccessibleButton>,
]
)
}
</div>
);
},
});

View File

@ -30,7 +30,7 @@ export default function SenderProfile(props) {
}
return (
<EmojiText className="mx_SenderProfile"
<EmojiText className="mx_SenderProfile" dir="auto"
onClick={props.onClick}>{`${name || ''} ${props.aux || ''}`}</EmojiText>
);
}

View File

@ -63,6 +63,19 @@ module.exports = React.createClass({
};
},
copyToClipboard: function(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
const successful = document.execCommand('copy');
} catch (err) {
console.log('Unable to copy');
}
document.body.removeChild(textArea);
},
componentDidMount: function() {
this._unmounted = false;
@ -81,6 +94,14 @@ 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);
};
}
}
},

View File

@ -24,6 +24,11 @@ import sdk from '../../../index';
module.exports = React.createClass({
displayName: 'TextualEvent',
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
var text = TextForEvent.textForEvent(this.props.mxEvent);

View File

@ -230,7 +230,7 @@ module.exports = React.createClass({
remote_aliases_section = (
<div>
<div className="mx_RoomSettings_aliasLabel">
Remote addresses for this room:
{_t("Remote addresses for this room:")}
</div>
<div className="mx_RoomSettings_aliasesTable">
{ this.state.remoteDomains.map((domain, i) => {

View File

@ -21,6 +21,8 @@ var Tinter = require('../../../Tinter');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
import dis from '../../../dispatcher';
var ROOM_COLORS = [
// magic room default values courtesy of Ribot
["#76cfa6", "#eaf5f0"],
@ -86,11 +88,7 @@ module.exports = React.createClass({
}
).catch(function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Saving room color settings is only available to registered users"
});
dis.dispatch({action: 'view_set_mxid'});
}
});
}

View File

@ -20,7 +20,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require("../../../index");
var Modal = require("../../../Modal");
var UserSettingsStore = require('../../../UserSettingsStore');
import { _t } from '../../../languageHandler';
import { _t, _tJsx } from '../../../languageHandler';
module.exports = React.createClass({
@ -131,12 +131,24 @@ module.exports = React.createClass({
</label>;
}
let urlPreviewText = null;
if (UserSettingsStore.getUrlPreviewsDisabled()) {
urlPreviewText = (
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{sub}</a>)
);
}
else {
urlPreviewText = (
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{sub}</a>)
);
}
return (
<div className="mx_RoomSettings_toggles">
<h3>{_t("URL Previews")}</h3>
<label>
You have <a href="#/settings">{ UserSettingsStore.getUrlPreviewsDisabled() ? 'disabled' : 'enabled' }</a> URL previews by default.
{urlPreviewText}
</label>
{ disableRoomPreviewUrls }
<label>

View File

@ -4,7 +4,7 @@ import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import isEqual from 'lodash/isEqual';
import sdk from '../../../index';
import type {Completion, SelectionRange} from '../../../autocomplete/Autocompleter';
import type {Completion} from '../../../autocomplete/Autocompleter';
import Q from 'q';
import {getCompletions} from '../../../autocomplete/Autocompleter';

View File

@ -19,7 +19,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from '../../../index';
import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils';
import { _t } from '../../../languageHandler';
import { _t, _tJsx} from '../../../languageHandler';
module.exports = React.createClass({
@ -78,7 +78,7 @@ module.exports = React.createClass({
fileDropTarget = (
<div className="mx_RoomView_fileDropTarget">
<div className="mx_RoomView_fileDropTargetLabel"
title="Drop File Here">
title={_t("Drop File Here")}>
<TintableSvg src="img/upload-big.svg" width="45" height="59"/>
<br/>
{_t("Drop file here to upload")}
@ -95,9 +95,14 @@ module.exports = React.createClass({
}
else {
joinText = (<span>
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}}
href="#">voice</a> or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video'); }}
href="#">video</a>.
{_tJsx(
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
[/<voiceText>(.*?)<\/voiceText>/, /<videoText>(.*?)<\/videoText>/],
[
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{sub}</a>,
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{sub}</a>,
]
)}
</span>);
}

View File

@ -21,6 +21,7 @@ var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
var PRESENCE_CLASS = {
@ -115,7 +116,7 @@ module.exports = React.createClass({
nameEl = (
<div className="mx_EntityTile_details">
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
<EmojiText element="div" className="mx_EntityTile_name_hover">{name}</EmojiText>
<EmojiText element="div" className="mx_EntityTile_name_hover" dir="auto">{name}</EmojiText>
<PresenceLabel activeAgo={ activeAgo }
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />
@ -124,7 +125,7 @@ module.exports = React.createClass({
}
else {
nameEl = (
<EmojiText element="div" className="mx_EntityTile_name">{name}</EmojiText>
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{name}</EmojiText>
);
}
@ -140,10 +141,10 @@ module.exports = React.createClass({
var power;
var powerLevel = this.props.powerLevel;
if (powerLevel >= 50 && powerLevel < 99) {
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt="Mod"/>;
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Moderator")}/>;
}
if (powerLevel >= 99) {
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt="Admin"/>;
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Admin")}/>;
}

View File

@ -38,6 +38,7 @@ var eventTileTypes = {
'm.call.answer' : 'messages.TextualEvent',
'm.call.hangup' : 'messages.TextualEvent',
'm.room.name' : 'messages.TextualEvent',
'm.room.avatar' : 'messages.RoomAvatarEvent',
'm.room.topic' : 'messages.TextualEvent',
'm.room.third_party_invite' : 'messages.TextualEvent',
'm.room.history_visibility' : 'messages.TextualEvent',
@ -380,6 +381,7 @@ module.exports = WithMatrixClient(React.createClass({
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
},
@ -481,27 +483,27 @@ module.exports = WithMatrixClient(React.createClass({
}
const editButton = (
<span className="mx_EventTile_editButton" title="Options" onClick={this.onEditClicked} />
<span className="mx_EventTile_editButton" title={ _t("Options") } onClick={this.onEditClicked} />
);
let e2e;
// cosmetic padlocks:
if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') {
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt="Encrypted by verified device" src="img/e2e-verified.svg" width="10" height="12" />;
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12" />;
}
// real padlocks
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Undecryptable" src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Undecryptable")} src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
}
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Encrypted by verified device" src="img/e2e-verified.svg" width="10" height="12"/>;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12"/>;
}
else {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Encrypted by unverified device" src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by an unverified device")} src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
}
}
else if (e2eEnabled) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Unencrypted message" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Unencrypted message")} src="img/e2e-unencrypted.svg" width="12" height="12"/>;
}
const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;

View File

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default class MemberDeviceInfo extends React.Component {
render() {
@ -25,19 +26,19 @@ export default class MemberDeviceInfo extends React.Component {
if (this.props.device.isBlocked()) {
indicator = (
<div className="mx_MemberDeviceInfo_blacklisted">
<img src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} alt="Blacklisted"/>
<img src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} alt={_t("Blacklisted")}/>
</div>
);
} else if (this.props.device.isVerified()) {
indicator = (
<div className="mx_MemberDeviceInfo_verified">
<img src="img/e2e-verified.svg" width="10" height="12" alt="Verified"/>
<img src="img/e2e-verified.svg" width="10" height="12" alt={_t("Verified")}/>
</div>
);
} else {
indicator = (
<div className="mx_MemberDeviceInfo_unverified">
<img src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }} alt="Unverified"/>
<img src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }} alt={_t("Unverified")}/>
</div>
);
}
@ -49,7 +50,7 @@ export default class MemberDeviceInfo extends React.Component {
// add the deviceId as a titletext to help with debugging
return (
<div className="mx_MemberDeviceInfo"
title={"device id: " + this.props.device.deviceId} >
title={_t("device id: ") + this.props.device.deviceId} >
<div className="mx_MemberDeviceInfo_deviceInfo">
<div className="mx_MemberDeviceInfo_deviceId">
{deviceName}

View File

@ -38,6 +38,8 @@ import Unread from '../../../Unread';
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
module.exports = WithMatrixClient(React.createClass({
displayName: 'MemberInfo',
@ -375,11 +377,7 @@ module.exports = WithMatrixClient(React.createClass({
console.log("Mod toggle success");
}, function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: _t("Please Register"),
description: _t("This action cannot be performed by a guest user. Please register to be able to do this") + ".",
});
dis.dispatch({action: 'view_set_mxid'});
} else {
console.error("Toggle moderator error:" + err);
Modal.createDialog(ErrorDialog, {
@ -436,7 +434,7 @@ module.exports = WithMatrixClient(React.createClass({
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself") }.<br/>
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br/>
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
@ -705,7 +703,7 @@ module.exports = WithMatrixClient(React.createClass({
if (kickButton || banButton || muteButton || giveModButton) {
adminTools =
<div>
<h3>Admin tools</h3>
<h3>{_t("Admin tools")}</h3>
<div className="mx_MemberInfo_buttons">
{muteButton}
@ -731,34 +729,36 @@ module.exports = WithMatrixClient(React.createClass({
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<div className="mx_MemberInfo">
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18"/></AccessibleButton>
<div className="mx_MemberInfo_avatar">
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
</div>
<EmojiText element="h2">{memberName}</EmojiText>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
<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">
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
</div>
<div className="mx_MemberInfo_profileField">
{ _t("Level") }: <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
<EmojiText element="h2">{memberName}</EmojiText>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
</div>
<div className="mx_MemberInfo_profileField">
{ _t("Level:") } <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={ presenceLastActiveAgo }
currentlyActive={ presenceCurrentlyActive }
presenceState={ presenceState } />
</div>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={ presenceLastActiveAgo }
currentlyActive={ presenceCurrentlyActive }
presenceState={ presenceState } />
</div>
</div>
{ adminTools }
{ adminTools }
{ startChat }
{ startChat }
{ this._renderDevices() }
{ this._renderDevices() }
{ spinner }
{ spinner }
</GeminiScrollbar>
</div>
);
}

View File

@ -28,12 +28,6 @@ var CallHandler = require("../../../CallHandler");
var Invite = require("../../../Invite");
var INITIAL_LOAD_NUM_MEMBERS = 30;
var SHARE_HISTORY_WARNING =
<span>
Newly invited users will see the history of this room. <br/>
If you'd prefer invited users not to see messages that were sent before they joined, <br/>
turn off, 'Share message history with new users' in the settings for this room.
</span>;
module.exports = React.createClass({
displayName: 'MemberList',
@ -355,7 +349,7 @@ module.exports = React.createClass({
if (invitedMemberTiles.length > 0) {
invitedSection = (
<div className="mx_MemberList_invited">
<h2>Invited</h2>
<h2>{ _t("Invited") }</h2>
<div className="mx_MemberList_wrapper">
{invitedMemberTiles}
</div>

View File

@ -22,6 +22,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
var Modal = require("../../../Modal");
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'MemberTile',
@ -63,7 +64,7 @@ module.exports = React.createClass({
},
getPowerLabel: function() {
return this.props.member.userId + " (power " + this.props.member.powerLevel + ")";
return _t("%(userName)s (power %(powerLevelNumber)s)", {userName: this.props.member.userId, powerLevelNumber: this.props.member.powerLevel});
},
render: function() {

Some files were not shown because too many files have changed in this diff Show More