diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles new file mode 100644 index 0000000000..f1b63d7367 --- /dev/null +++ b/.eslintignore.errorfiles @@ -0,0 +1,187 @@ +# 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/MatrixChat.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/structures/UserSettings.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 diff --git a/.eslintrc.js b/.eslintrc.js index 6cd0e1015e..74790a2964 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,7 +64,7 @@ module.exports = { // to JSX. ignorePattern: '^\\s*<', ignoreComments: true, - code: 90, + code: 120, }], "valid-jsdoc": ["warn"], "new-cap": ["warn"], diff --git a/.gitignore b/.gitignore index 5139d614ad..f828c37393 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,8 @@ npm-debug.log # test reports created by karma /karma-reports + +/.idea +/src/component-index.js + +.DS_Store diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index c280044246..4296c72e6c 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -9,11 +9,16 @@ set -ev RIOT_WEB_DIR=riot-web REACT_SDK_DIR=`pwd` -git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git \ +curbranch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}" +echo "Determined branch to be $curbranch" + +git clone https://github.com/vector-im/riot-web.git \ "$RIOT_WEB_DIR" cd "$RIOT_WEB_DIR" +git checkout "$curbranch" || git checkout develop + mkdir node_modules npm install diff --git a/.travis.yml b/.travis.yml index 9a8f804644..918cec696b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 97dda666de..66e4627afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,336 @@ +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) + + * No changes + + +Changes in [0.8.9-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9-rc.1) (2017-05-19) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8...v0.8.9-rc.1) + + * Prevent an exception getting scroll node + [\#902](https://github.com/matrix-org/matrix-react-sdk/pull/902) + * Fix a few remaining snags with country dd + [\#901](https://github.com/matrix-org/matrix-react-sdk/pull/901) + * Add left_aligned class to CountryDropdown + [\#900](https://github.com/matrix-org/matrix-react-sdk/pull/900) + * Swap to new flag files (which are stored as GB.png) + [\#899](https://github.com/matrix-org/matrix-react-sdk/pull/899) + * Improve phone number country dropdown for registration and login (Act. 2, + Return of the Prefix) + [\#897](https://github.com/matrix-org/matrix-react-sdk/pull/897) + * Support for pasting files into normal composer + [\#892](https://github.com/matrix-org/matrix-react-sdk/pull/892) + * tell guests they can't use filepanel until they register + [\#887](https://github.com/matrix-org/matrix-react-sdk/pull/887) + * Prevent reskindex -w from running when file names have not changed + [\#888](https://github.com/matrix-org/matrix-react-sdk/pull/888) + * I broke UserSettings for webpack-dev-server + [\#884](https://github.com/matrix-org/matrix-react-sdk/pull/884) + * various fixes to RoomHeader + [\#880](https://github.com/matrix-org/matrix-react-sdk/pull/880) + * remove /me whether or not it has a space after it + [\#885](https://github.com/matrix-org/matrix-react-sdk/pull/885) + * show error if we can't set a filter because no room + [\#883](https://github.com/matrix-org/matrix-react-sdk/pull/883) + * Fix RM not updating if RR event unpaginated + [\#874](https://github.com/matrix-org/matrix-react-sdk/pull/874) + * change roomsettings wording + [\#878](https://github.com/matrix-org/matrix-react-sdk/pull/878) + * make reskindex windows friendly + [\#875](https://github.com/matrix-org/matrix-react-sdk/pull/875) + * Fixes 2 issues with Dialog closing + [\#867](https://github.com/matrix-org/matrix-react-sdk/pull/867) + * Automatic Reskindex + [\#871](https://github.com/matrix-org/matrix-react-sdk/pull/871) + * Put room name in 'leave room' confirmation dialog + [\#873](https://github.com/matrix-org/matrix-react-sdk/pull/873) + * Fix this/self fail in LeftPanel + [\#872](https://github.com/matrix-org/matrix-react-sdk/pull/872) + * Don't show null URL previews + [\#870](https://github.com/matrix-org/matrix-react-sdk/pull/870) + * Fix keys for AddressSelector + [\#869](https://github.com/matrix-org/matrix-react-sdk/pull/869) + * Make left panel better for new users (mk II) + [\#859](https://github.com/matrix-org/matrix-react-sdk/pull/859) + * Explicitly save composer content onUnload + [\#866](https://github.com/matrix-org/matrix-react-sdk/pull/866) + * Warn on unload + [\#851](https://github.com/matrix-org/matrix-react-sdk/pull/851) + * Log deviceid at login + [\#862](https://github.com/matrix-org/matrix-react-sdk/pull/862) + * Guests can't send RR so no point trying + [\#860](https://github.com/matrix-org/matrix-react-sdk/pull/860) + * Remove babelcheck + [\#861](https://github.com/matrix-org/matrix-react-sdk/pull/861) + * T3chguy/settings versions improvements + [\#857](https://github.com/matrix-org/matrix-react-sdk/pull/857) + * Change max-len 90->120 + [\#852](https://github.com/matrix-org/matrix-react-sdk/pull/852) + * Remove DM-guessing code + [\#829](https://github.com/matrix-org/matrix-react-sdk/pull/829) + * Fix jumping to an unread event when in MELS + [\#855](https://github.com/matrix-org/matrix-react-sdk/pull/855) + * Validate phone number on login + [\#856](https://github.com/matrix-org/matrix-react-sdk/pull/856) + * Failed to enable HTML5 Notifications Error Dialogs + [\#827](https://github.com/matrix-org/matrix-react-sdk/pull/827) + * Pin filesize ver to fix break upstream + [\#854](https://github.com/matrix-org/matrix-react-sdk/pull/854) + * Improve RoomDirectory Look & Feel + [\#848](https://github.com/matrix-org/matrix-react-sdk/pull/848) + * Only show jumpToReadMarker bar when RM !== RR + [\#845](https://github.com/matrix-org/matrix-react-sdk/pull/845) + * Allow MELS to have its own RM + [\#846](https://github.com/matrix-org/matrix-react-sdk/pull/846) + * Use document.onkeydown instead of onkeypress + [\#844](https://github.com/matrix-org/matrix-react-sdk/pull/844) + * (Room)?Avatar: Request 96x96 avatars on high DPI screens + [\#808](https://github.com/matrix-org/matrix-react-sdk/pull/808) + * Add mx_EventTile_emote class + [\#842](https://github.com/matrix-org/matrix-react-sdk/pull/842) + * Fix dialog reappearing after hitting Enter + [\#841](https://github.com/matrix-org/matrix-react-sdk/pull/841) + * Fix spinner that shows until the first sync + [\#840](https://github.com/matrix-org/matrix-react-sdk/pull/840) + * Show spinner until first sync has completed + [\#839](https://github.com/matrix-org/matrix-react-sdk/pull/839) + * Style fixes for LoggedInView + [\#838](https://github.com/matrix-org/matrix-react-sdk/pull/838) + * Fix specifying custom server for registration + [\#834](https://github.com/matrix-org/matrix-react-sdk/pull/834) + * Improve country dropdown UX and expose +prefix + [\#833](https://github.com/matrix-org/matrix-react-sdk/pull/833) + * Fix user settings store + [\#836](https://github.com/matrix-org/matrix-react-sdk/pull/836) + * show the room name in the UDE Dialog + [\#832](https://github.com/matrix-org/matrix-react-sdk/pull/832) + * summarise profile changes in MELS + [\#826](https://github.com/matrix-org/matrix-react-sdk/pull/826) + * Transform h1 and h2 tags to h3 tags + [\#820](https://github.com/matrix-org/matrix-react-sdk/pull/820) + * limit our keyboard shortcut modifiers correctly + [\#825](https://github.com/matrix-org/matrix-react-sdk/pull/825) + * Specify cross platform regexes and add olm to noParse + [\#823](https://github.com/matrix-org/matrix-react-sdk/pull/823) + * Remember element that was in focus before rendering dialog + [\#822](https://github.com/matrix-org/matrix-react-sdk/pull/822) + * move user settings outward and use built in read receipts disabling + [\#824](https://github.com/matrix-org/matrix-react-sdk/pull/824) + * File Download Consistency + [\#802](https://github.com/matrix-org/matrix-react-sdk/pull/802) + * Show Access Token under Advanced in Settings + [\#806](https://github.com/matrix-org/matrix-react-sdk/pull/806) + * Link tags/commit hashes in the UserSettings version section + [\#810](https://github.com/matrix-org/matrix-react-sdk/pull/810) + * On return to RoomView from auxPanel, send focus back to Composer + [\#813](https://github.com/matrix-org/matrix-react-sdk/pull/813) + * Change presence status labels to 'for' instead of 'ago' + [\#817](https://github.com/matrix-org/matrix-react-sdk/pull/817) + * Disable Scalar Integrations if urls passed to it are falsey + [\#816](https://github.com/matrix-org/matrix-react-sdk/pull/816) + * Add option to hide other people's read receipts. + [\#818](https://github.com/matrix-org/matrix-react-sdk/pull/818) + * Add option to not send typing notifications + [\#819](https://github.com/matrix-org/matrix-react-sdk/pull/819) + * Sync RM across instances of Riot + [\#805](https://github.com/matrix-org/matrix-react-sdk/pull/805) + * First iteration on improving login UI + [\#811](https://github.com/matrix-org/matrix-react-sdk/pull/811) + * focus on composer after jumping to bottom + [\#809](https://github.com/matrix-org/matrix-react-sdk/pull/809) + * Improve RoomList performance via side-stepping React + [\#807](https://github.com/matrix-org/matrix-react-sdk/pull/807) + * Don't show link preview when link is inside of a quote + [\#762](https://github.com/matrix-org/matrix-react-sdk/pull/762) + * Escape closes UserSettings + [\#765](https://github.com/matrix-org/matrix-react-sdk/pull/765) + * Implement user power-level changes in timeline + [\#794](https://github.com/matrix-org/matrix-react-sdk/pull/794) + Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8) diff --git a/README.md b/README.md index 3627225299..0f5ef73365 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ In the interim, `vector-im/riot-web` and `matrix-org/matrix-react-sdk` should be considered as a single project (for instance, matrix-react-sdk bugs are currently filed against vector-im/riot-web rather than this project). +Translation Status +================== +[![translationsstatus](https://translate.nordgedanken.de/widgets/riot-web/-/multi-auto.svg)](https://translate.nordgedanken.de/engage/riot-web/?utm_source=widget) + Developer Guide =============== @@ -190,4 +194,3 @@ Alternative instructions: * Create an index.html file pulling in your compiled javascript and the CSS bundle from the skin you use. For now, you'll also need to manually import CSS from any skins that your skin inherts from. - diff --git a/code_style.md b/code_style.md index f0eca75ffc..2cac303e54 100644 --- a/code_style.md +++ b/code_style.md @@ -69,25 +69,41 @@ General Style console.log("I am a fish"); // Bad } ``` +- No new line before else, catch, finally, etc: + + ```javascript + if (x) { + console.log("I am a fish"); + } else { + console.log("I am a chimp"); // Good + } + + if (x) { + console.log("I am a fish"); + } + else { + console.log("I am a chimp"); // Bad + } + ``` - Declare one variable per var statement (consistent with Node). Unless they are simple and closely related. If you put the next declaration on a new line, treat yourself to another `var`: ```javascript - var key = "foo", + const key = "foo", comparator = function(x, y) { return x - y; }; // Bad - var key = "foo"; - var comparator = function(x, y) { + const key = "foo"; + const comparator = function(x, y) { return x - y; }; // Good - var x = 0, y = 0; // Fine + let x = 0, y = 0; // Fine - var x = 0; - var y = 0; // Also fine + let x = 0; + let y = 0; // Also fine ``` - A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: diff --git a/header b/header index 060709b82e..beee1ebe89 100644 --- a/header +++ b/header @@ -1,5 +1,6 @@ /* Copyright 2015, 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. diff --git a/jenkins.sh b/jenkins.sh index 6a77911c27..d9bb62855b 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -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 diff --git a/karma.conf.js b/karma.conf.js index 6d3047bb3b..d544248332 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -55,11 +55,18 @@ module.exports = function (config) { // some images to reduce noise from the tests {pattern: 'test/img/*', watched: false, included: false, served: true, nocache: false}, + // translation files + {pattern: 'src/i18n/strings/*', watcheed: false, included: false, served: true}, + {pattern: 'test/i18n/*', watched: false, included: false, served: true}, ], - // redirect img links to the karma server proxies: { + // redirect img links to the karma server "/img/": "/base/test/img/", + // special languages.json file for the tests + "/i18n/languages.json": "/base/test/i18n/languages.json", + // and redirect i18n requests + "/i18n/": "/base/src/i18n/strings/", }, // list of files to exclude @@ -135,17 +142,24 @@ module.exports = function (config) { }, ], noParse: [ + // for cross platform compatibility use [\\\/] as the path separator + // this ensures that the regex trips on both Windows and *nix + // don't parse the languages within highlight.js. They // cause stack overflows // (https://github.com/webpack/webpack/issues/1721), and // there is no need for webpack to parse them - they can // just be included as-is. - /highlight\.js\/lib\/languages/, + /highlight\.js[\\\/]lib[\\\/]languages/, + + // olm takes ages for webpack to process, and it's already heavily + // optimised, so there is little to gain by us uglifying it. + /olm[\\\/](javascript[\\\/])?olm\.js$/, // also disable parsing for sinon, because it // tries to do voodoo with 'require' which upsets // webpack (https://github.com/webpack/webpack/issues/304) - /sinon\/pkg\/sinon\.js$/, + /sinon[\\\/]pkg[\\\/]sinon\.js$/, ], }, resolve: { @@ -159,11 +173,15 @@ module.exports = function (config) { 'sinon': 'sinon/pkg/sinon.js', }, root: [ - path.resolve('./src'), path.resolve('./test'), ], }, 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: { diff --git a/package.json b/package.json index 00dc902cc9..15a903c25a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.8", + "version": "0.9.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -31,9 +31,11 @@ "reskindex": "scripts/reskindex.js" }, "scripts": { - "reskindex": "scripts/reskindex.js -h header", - "build": "node scripts/babelcheck.js && babel src -d lib --source-maps", - "start": "node scripts/babelcheck.js && babel src -w -d lib --source-maps", + "reskindex": "node scripts/reskindex.js -h header", + "reskindex:watch": "node scripts/reskindex.js -h header -w", + "build": "npm run reskindex && babel src -d lib --source-maps", + "build:watch": "babel src -w -d lib --source-maps", + "start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"", "lint": "eslint src/", "lintall": "eslint src/ test/", "clean": "rimraf lib", @@ -48,12 +50,13 @@ "browser-request": "^0.3.3", "classnames": "^2.1.2", "commonmark": "^0.27.0", + "counterpart": "^0.18.0", "draft-js": "^0.8.1", "draft-js-export-html": "^0.5.0", "draft-js-export-markdown": "^0.2.0", "emojione": "2.2.3", "file-saver": "^1.3.3", - "filesize": "^3.1.2", + "filesize": "3.5.6", "flux": "^2.0.3", "fuse.js": "^2.2.0", "glob": "^5.0.14", @@ -61,8 +64,9 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.7", + "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", @@ -88,6 +92,7 @@ "babel-preset-es2016": "^6.11.3", "babel-preset-es2017": "^6.14.0", "babel-preset-react": "^6.11.1", + "chokidar": "^1.6.1", "eslint": "^3.13.1", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^4.0.1", @@ -104,6 +109,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.7.0", "mocha": "^2.4.5", + "parallelshell": "^1.2.0", "phantomjs-prebuilt": "^2.1.7", "react-addons-test-utils": "^15.4.0", "require-json": "0.0.1", diff --git a/scripts/babelcheck.js b/scripts/babelcheck.js deleted file mode 100644 index 14e4a28a70..0000000000 --- a/scripts/babelcheck.js +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env node - -var exec = require('child_process').exec; - -// Makes sure the babel executable in the path is babel 6 (or greater), not -// babel 5, which it is if you upgrade from an older version of react-sdk and -// run 'npm install' since the package has changed to babel-cli, so 'babel' -// remains installed and the executable in node_modules/.bin remains as babel -// 5. - -exec("babel -V", function (error, stdout, stderr) { - if ((error && error.code) || parseInt(stdout.substr(0,1), 10) < 6) { - console.log("\033[31m\033[1m"+ - '*****************************************\n'+ - '* matrix-react-sdk has moved to babel 6 *\n'+ - '* Please "rm -rf node_modules && npm i" *\n'+ - '* then restore links as appropriate *\n'+ - '*****************************************\n'+ - "\033[91m"); - process.exit(1); - } -}); diff --git a/scripts/check-i18n.pl b/scripts/check-i18n.pl new file mode 100755 index 0000000000..fa11bc5292 --- /dev/null +++ b/scripts/check-i18n.pl @@ -0,0 +1,192 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Cwd 'abs_path'; + +# script which checks how out of sync the i18ns are drifting + +# example i18n format: +# "%(oneUser)sleft": "%(oneUser)sleft", + +$|=1; + +$0 =~ /^(.*\/)/; +my $i18ndir = abs_path($1."/../src/i18n/strings"); +my $srcdir = abs_path($1."/../src"); + +my $en = read_i18n($i18ndir."/en_EN.json"); + +my $src_strings = read_src_strings($srcdir); +my $src = {}; + +print "Checking strings in src\n"; +foreach my $tuple (@$src_strings) { + my ($s, $file) = (@$tuple); + $src->{$s} = $file; + if (!$en->{$s}) { + if ($en->{$s . '.'}) { + printf ("%50s %24s\t%s\n", $file, "en_EN has fullstop!", $s); + } + else { + $s =~ /^(.*)\.?$/; + if ($en->{$1}) { + printf ("%50s %24s\t%s\n", $file, "en_EN lacks fullstop!", $s); + } + else { + printf ("%50s %24s\t%s\n", $file, "Translation missing!", $s); + } + } + } +} + +print "\nChecking en_EN\n"; +my $count = 0; +my $remaining_src = {}; +foreach (keys %$src) { $remaining_src->{$_}++ }; + +foreach my $k (sort keys %$en) { + # crappy heuristic to ignore country codes for now... + next if ($k =~ /^(..|..-..)$/); + + if ($en->{$k} ne $k) { + printf ("%50s %24s\t%s\n", "en_EN", "en_EN is not symmetrical", $k); + } + + if (!$src->{$k}) { + if ($src->{$k. '.'}) { + printf ("%50s %24s\t%s\n", $src->{$k. '.'}, "src has fullstop!", $k); + } + else { + $k =~ /^(.*)\.?$/; + if ($src->{$1}) { + printf ("%50s %24s\t%s\n", $src->{$1}, "src lacks fullstop!", $k); + } + else { + printf ("%50s %24s\t%s\n", '???', "Not present in src?", $k); + } + } + } + else { + $count++; + delete $remaining_src->{$k}; + } +} +printf ("$count/" . (scalar keys %$src) . " strings found in src are present in en_EN\n"); +foreach (keys %$remaining_src) { + print "missing: $_\n"; +} + +opendir(DIR, $i18ndir) || die $!; +my @files = readdir(DIR); +closedir(DIR); +foreach my $lang (grep { -f "$i18ndir/$_" && !/(basefile|en_EN)\.json/ } @files) { + print "\nChecking $lang\n"; + + my $map = read_i18n($i18ndir."/".$lang); + my $count = 0; + + my $remaining_en = {}; + foreach (keys %$en) { $remaining_en->{$_}++ }; + + foreach my $k (sort keys %$map) { + { + no warnings 'uninitialized'; + my $vars = {}; + while ($k =~ /%\((.*?)\)s/g) { + $vars->{$1}++; + } + while ($map->{$k} =~ /%\((.*?)\)s/g) { + $vars->{$1}--; + } + foreach my $var (keys %$vars) { + if ($vars->{$var} != 0) { + printf ("%10s %24s\t%s\n", $lang, "Broken var ($var)s", $k); + } + } + } + + if ($en->{$k}) { + if ($map->{$k} eq $k) { + printf ("%10s %24s\t%s\n", $lang, "Untranslated string?", $k); + } + $count++; + delete $remaining_en->{$k}; + } + else { + if ($en->{$k . "."}) { + printf ("%10s %24s\t%s\n", $lang, "en_EN has fullstop!", $k); + next; + } + + $k =~ /^(.*)\.?$/; + if ($en->{$1}) { + printf ("%10s %24s\t%s\n", $lang, "en_EN lacks fullstop!", $k); + next; + } + + printf ("%10s %24s\t%s\n", $lang, "Not present in en_EN", $k); + } + } + + if (scalar keys %$remaining_en < 100) { + foreach (keys %$remaining_en) { + printf ("%10s %24s\t%s\n", $lang, "Not yet translated", $_); + } + } + + printf ("$count/" . (scalar keys %$en) . " strings translated\n"); +} + +sub read_i18n { + my $path = shift; + my $map = {}; + $path =~ /.*\/(.*)$/; + my $lang = $1; + + open(FILE, "<", $path) || die $!; + while() { + if ($_ =~ m/^(\s+)"(.*?)"(: *)"(.*?)"(,?)$/) { + my ($indent, $src, $colon, $dst, $comma) = ($1, $2, $3, $4, $5); + $src =~ s/\\"/"/g; + $dst =~ s/\\"/"/g; + + if ($map->{$src}) { + printf ("%10s %24s\t%s\n", $lang, "Duplicate translation!", $src); + } + $map->{$src} = $dst; + } + } + close(FILE); + + return $map; +} + +sub read_src_strings { + my $path = shift; + + use File::Find; + use File::Slurp; + + my $strings = []; + + my @files; + find( sub { push @files, $File::Find::name if (-f $_ && /\.jsx?$/) }, $path ); + foreach my $file (@files) { + my $src = read_file($file); + $src =~ s/'\s*\+\s*'//g; + $src =~ s/"\s*\+\s*"//g; + + $file =~ s/^.*\/src/src/; + while ($src =~ /_t(?:Jsx)?\(\s*'(.*?[^\\])'/sg) { + my $s = $1; + $s =~ s/\\'/'/g; + push @$strings, [$s, $file]; + } + while ($src =~ /_t(?:Jsx)?\(\s*"(.*?[^\\])"/sg) { + push @$strings, [$1, $file]; + } + } + + return $strings; +} \ No newline at end of file diff --git a/scripts/fix-i18n.pl b/scripts/fix-i18n.pl new file mode 100755 index 0000000000..def352463d --- /dev/null +++ b/scripts/fix-i18n.pl @@ -0,0 +1,114 @@ +#!/usr/bin/perl -ni + +use strict; +use warnings; + +# script which synchronises i18n strings to include punctuation. +# i've cherry-picked ones which seem to have diverged between the different translations +# from TextForEvent, causing missing events all over the place + +BEGIN { +$::fixups = [split(/\n/, < 0) | .filePath' | + sed -e 's/.*matrix-react-sdk\///'; +} > "$out" diff --git a/scripts/reskindex.js b/scripts/reskindex.js index f9cbc2a711..833151a298 100755 --- a/scripts/reskindex.js +++ b/scripts/reskindex.js @@ -1,53 +1,99 @@ #!/usr/bin/env node - var fs = require('fs'); var path = require('path'); var glob = require('glob'); - var args = require('optimist').argv; - -var header = args.h || args.header; - -var componentsDir = path.join('src', 'components'); +var chokidar = require('chokidar'); var componentIndex = path.join('src', 'component-index.js'); +var componentIndexTmp = componentIndex+".tmp"; +var componentsDir = path.join('src', 'components'); +var componentGlob = '**/*.js'; +var prevFiles = []; -var packageJson = JSON.parse(fs.readFileSync('./package.json')); +function reskindex() { + var files = glob.sync(componentGlob, {cwd: componentsDir}).sort(); + if (!filesHaveChanged(files, prevFiles)) { + return; + } + prevFiles = files; -var strm = fs.createWriteStream(componentIndex); + var header = args.h || args.header; + var packageJson = JSON.parse(fs.readFileSync('./package.json')); -if (header) { - strm.write(fs.readFileSync(header)); - strm.write('\n'); + var strm = fs.createWriteStream(componentIndexTmp); + + if (header) { + strm.write(fs.readFileSync(header)); + strm.write('\n'); + } + + strm.write("/*\n"); + strm.write(" * THIS FILE IS AUTO-GENERATED\n"); + strm.write(" * You can edit it you like, but your changes will be overwritten,\n"); + strm.write(" * so you'd just be trying to swim upstream like a salmon.\n"); + strm.write(" * You are not a salmon.\n"); + strm.write(" */\n\n"); + + if (packageJson['matrix-react-parent']) { + const parentIndex = packageJson['matrix-react-parent'] + + '/lib/component-index'; + strm.write( +`let components = require('${parentIndex}').components; +if (!components) { + throw new Error("'${parentIndex}' didn't export components"); +} +`); + } else { + strm.write("let components = {};\n"); + } + + for (var i = 0; i < files.length; ++i) { + var file = files[i].replace('.js', ''); + + var moduleName = (file.replace(/\//g, '.')); + var importName = moduleName.replace(/\./g, "$"); + + strm.write("import " + importName + " from './components/" + file + "';\n"); + strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");"); + strm.write('\n'); + strm.uncork(); + } + + strm.write("export {components};\n"); + strm.end(); + fs.rename(componentIndexTmp, componentIndex, function(err) { + if(err) { + console.error("Error moving new index into place: " + err); + } else { + console.log('Reskindex: completed'); + } + }); } -strm.write("/*\n"); -strm.write(" * THIS FILE IS AUTO-GENERATED\n"); -strm.write(" * You can edit it you like, but your changes will be overwritten,\n"); -strm.write(" * so you'd just be trying to swim upstream like a salmon.\n"); -strm.write(" * You are not a salmon.\n"); -strm.write(" *\n"); -strm.write(" * To update it, run:\n"); -strm.write(" * ./reskindex.js -h header\n"); -strm.write(" */\n\n"); - -if (packageJson['matrix-react-parent']) { - strm.write("module.exports.components = require('"+packageJson['matrix-react-parent']+"/lib/component-index').components;\n\n"); -} else { - strm.write("module.exports.components = {};\n"); +// Expects both arrays of file names to be sorted +function filesHaveChanged(files, prevFiles) { + if (files.length !== prevFiles.length) { + return true; + } + // Check for name changes + for (var i = 0; i < files.length; i++) { + if (prevFiles[i] !== files[i]) { + return true; + } + } + return false; } -var files = glob.sync('**/*.js', {cwd: componentsDir}).sort(); -for (var i = 0; i < files.length; ++i) { - var file = files[i].replace('.js', ''); - - var moduleName = (file.replace(/\//g, '.')); - var importName = moduleName.replace(/\./g, "$"); - - strm.write("import " + importName + " from './components/" + file + "';\n"); - strm.write(importName + " && (module.exports.components['"+moduleName+"'] = " + importName + ");"); - strm.write('\n'); - strm.uncork(); +// -w indicates watch mode where any FS events will trigger reskindex +if (!args.w) { + reskindex(); + return; } -strm.end(); +var watchDebouncer = null; +chokidar.watch(path.join(componentsDir, componentGlob)).on('all', (event, path) => { + if (path === componentIndex) return; + if (watchDebouncer) clearTimeout(watchDebouncer); + watchDebouncer = setTimeout(reskindex, 1000); +}); diff --git a/scripts/travis.sh b/scripts/travis.sh new file mode 100755 index 0000000000..f349b06ad5 --- /dev/null +++ b/scripts/travis.sh @@ -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 diff --git a/src/AddThreepid.js b/src/AddThreepid.js index c89de4f5fa..8be7a19b13 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -16,6 +16,7 @@ limitations under the License. */ var MatrixClientPeg = require("./MatrixClientPeg"); +import { _t } from './languageHandler'; /** * Allows a user to add a third party identifier to their Home Server and, @@ -44,7 +45,7 @@ class AddThreepid { return res; }, function(err) { if (err.errcode == 'M_THREEPID_IN_USE') { - err.message = "This email address is already in use"; + err.message = _t('This email address is already in use'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } @@ -69,7 +70,7 @@ class AddThreepid { return res; }, function(err) { if (err.errcode == 'M_THREEPID_IN_USE') { - err.message = "This phone number is already in use"; + err.message = _t('This phone number is already in use'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } @@ -91,7 +92,7 @@ class AddThreepid { id_server: identityServerDomain }, this.bind).catch(function(err) { if (err.httpStatus === 401) { - err.message = "Failed to verify email address: make sure you clicked the link in the email"; + err.message = _t('Failed to verify email address: make sure you clicked the link in the email'); } else if (err.httpStatus) { err.message += ` (Status ${err.httpStatus})`; diff --git a/src/Analytics.js b/src/Analytics.js new file mode 100644 index 0000000000..92691da1ea --- /dev/null +++ b/src/Analytics.js @@ -0,0 +1,153 @@ +/* + 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 { getCurrentLanguage } from './languageHandler'; +import MatrixClientPeg from './MatrixClientPeg'; +import PlatformPeg from './PlatformPeg'; +import SdkConfig from './SdkConfig'; + +function getRedactedUrl() { + const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/"); + // hardcoded url to make piwik happy + return 'https://riot.im/app/' + redactedHash; +} + +const customVariables = { + 'App Platform': 1, + 'App Version': 2, + 'User Type': 3, + 'Chosen Language': 4, + 'Instance': 5, +}; + + +class Analytics { + constructor() { + this._paq = null; + this.disabled = true; + this.firstPage = true; + } + + /** + * Enable Analytics if initialized but disabled + * otherwise try and initalize, no-op if piwik config missing + */ + enable() { + if (this._paq || this._init()) { + this.disabled = false; + } + } + + /** + * Disable Analytics calls, will not fully unload Piwik until a refresh, + * but this is second best, Piwik should not pull anything implicitly. + */ + disable() { + this.trackEvent('Analytics', 'opt-out'); + this.disabled = true; + } + + _init() { + const config = SdkConfig.get(); + if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return; + + const url = config.piwik.url; + const siteId = config.piwik.siteId; + const self = this; + + window._paq = this._paq = window._paq || []; + + this._paq.push(['setTrackerUrl', url+'piwik.php']); + this._paq.push(['setSiteId', siteId]); + + this._paq.push(['trackAllContentImpressions']); + this._paq.push(['discardHashTag', false]); + this._paq.push(['enableHeartBeatTimer']); + this._paq.push(['enableLinkTracking', true]); + + const platform = PlatformPeg.get(); + this._setVisitVariable('App Platform', platform.getHumanReadableName()); + platform.getAppVersion().then((version) => { + this._setVisitVariable('App Version', version); + }).catch(() => { + this._setVisitVariable('App Version', 'unknown'); + }); + + 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]; + g.type='text/javascript'; g.async=true; g.defer=true; g.src=url+'piwik.js'; + + g.onload = function() { + console.log('Initialised anonymous analytics'); + self._paq = window._paq; + }; + + s.parentNode.insertBefore(g, s); + })(); + + return true; + } + + trackPageChange() { + if (this.disabled) return; + if (this.firstPage) { + // De-duplicate first page + // router seems to hit the fn twice + this.firstPage = false; + return; + } + this._paq.push(['setCustomUrl', getRedactedUrl()]); + this._paq.push(['trackPageView']); + } + + trackEvent(category, action, name) { + if (this.disabled) return; + this._paq.push(['trackEvent', category, action, name]); + } + + logout() { + if (this.disabled) return; + this._paq.push(['deleteCookies']); + } + + login() { // not used currently + const cli = MatrixClientPeg.get(); + if (this.disabled || !cli) return; + + this._paq.push(['setUserId', `@${cli.getUserIdLocalpart()}:${cli.getDomain()}`]); + } + + _setVisitVariable(key, value) { + this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']); + } + + setGuest(guest) { + if (this.disabled) return; + this._setVisitVariable('User Type', guest ? 'Guest' : 'Logged In'); + } +} + +if (!global.mxAnalytics) { + global.mxAnalytics = new Analytics(); +} +module.exports = global.mxAnalytics; diff --git a/src/Avatar.js b/src/Avatar.js index 76f5e55ff0..c0127d49af 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -22,8 +22,8 @@ module.exports = { avatarUrlForMember: function(member, width, height, resizeMethod) { var url = member.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), - width, - height, + Math.floor(width * window.devicePixelRatio), + Math.floor(height * window.devicePixelRatio), resizeMethod, false, false @@ -40,7 +40,9 @@ module.exports = { avatarUrlForUser: function(user, width, height, resizeMethod) { var url = ContentRepo.getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, - width, height, resizeMethod + Math.floor(width * window.devicePixelRatio), + Math.floor(height * window.devicePixelRatio), + resizeMethod ); if (!url || url.length === 0) { return null; @@ -57,4 +59,3 @@ module.exports = { return 'img/' + images[total % images.length] + '.png'; } }; - diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 6eed22f436..d0d8e0c74e 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -29,6 +29,11 @@ export default class BasePlatform { this.errorDidOccur = false; } + // Used primarily for Analytics + getHumanReadableName(): string { + return 'Base Platform'; + } + setNotificationCount(count: number) { this.notificationCount = count; } @@ -66,11 +71,14 @@ 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. */ - getAppVersion() { + getAppVersion(): Promise { throw new Error("getAppVersion not implemented!"); } @@ -79,10 +87,12 @@ export default class BasePlatform { * with getUserMedia, return a string explaining why not. * Otherwise, return null. */ - screenCaptureErrorString() { + screenCaptureErrorString(): string { return "Not implemented"; } + isElectron(): boolean { return false; } + /** * Restarts the application, without neccessarily reloading * any application code diff --git a/src/CallHandler.js b/src/CallHandler.js index 42cc681d08..b2ccf65df7 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -55,6 +55,7 @@ var MatrixClientPeg = require('./MatrixClientPeg'); var PlatformPeg = require("./PlatformPeg"); var Modal = require('./Modal'); var sdk = require('./index'); +import { _t } from './languageHandler'; var Matrix = require("matrix-js-sdk"); var dis = require("./dispatcher"); @@ -142,8 +143,8 @@ function _setCallListeners(call) { play("busyAudio"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Call Timeout", - description: "The remote side failed to pick up." + title: _t('Call Timeout'), + description: _t('The remote side failed to pick up') + '.', }); } else if (oldState === "invite_sent") { @@ -179,7 +180,8 @@ function _setCallState(call, roomId, status) { } dis.dispatch({ action: 'call_state', - room_id: roomId + room_id: roomId, + state: status, }); } @@ -203,8 +205,8 @@ function _onAction(payload) { console.log("Can't capture screen: " + screenCapErrorString); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Unable to capture screen", - description: screenCapErrorString + title: _t('Unable to capture screen'), + description: screenCapErrorString, }); return; } @@ -223,8 +225,8 @@ function _onAction(payload) { if (module.exports.getAnyActiveCall()) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Existing Call", - description: "You are already in a call." + title: _t('Existing Call'), + description: _t('You are already in a call.'), }); return; // don't allow >1 call to be placed. } @@ -233,8 +235,8 @@ function _onAction(payload) { if (!MatrixClientPeg.get().supportsVoip()) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "VoIP is unsupported", - description: "You cannot place VoIP calls in this browser." + title: _t('VoIP is unsupported'), + description: _t('You cannot place VoIP calls in this browser.'), }); return; } @@ -249,7 +251,7 @@ function _onAction(payload) { if (members.length <= 1) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - description: "You cannot place a call with yourself." + description: _t('You cannot place a call with yourself.'), }); return; } @@ -275,14 +277,14 @@ function _onAction(payload) { if (!ConferenceHandler) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - description: "Conference calls are not supported in this client" + description: _t('Conference calls are not supported in this client'), }); } else if (!MatrixClientPeg.get().supportsVoip()) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "VoIP is unsupported", - description: "You cannot place VoIP calls in this browser." + title: _t('VoIP is unsupported'), + description: _t('You cannot place VoIP calls in this browser.'), }); } else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) { @@ -294,14 +296,14 @@ function _onAction(payload) { // Therefore we disable conference calling in E2E rooms. const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - description: "Conference calls are not supported in encrypted rooms", + description: _t('Conference calls are not supported in encrypted rooms'), }); } else { var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createDialog(QuestionDialog, { - title: "Warning!", - description: "Conference calling is in development and may not be reliable.", + title: _t('Warning!'), + description: _t('Conference calling is in development and may not be reliable.'), onFinished: confirm=>{ if (confirm) { ConferenceHandler.createNewMatrixCall( @@ -312,8 +314,8 @@ function _onAction(payload) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Conference call failed: " + err); Modal.createDialog(ErrorDialog, { - title: "Failed to set up conference call", - description: "Conference call failed.", + title: _t('Failed to set up conference call'), + description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''), }); }); } diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js new file mode 100644 index 0000000000..839b496845 --- /dev/null +++ b/src/CallMediaHandler.js @@ -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); + }, +}; diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 4ab982c98f..315c312b9f 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -21,6 +21,7 @@ var extend = require('./extend'); var dis = require('./dispatcher'); var MatrixClientPeg = require('./MatrixClientPeg'); var sdk = require('./index'); +import { _t } from './languageHandler'; var Modal = require('./Modal'); var encrypt = require("browser-encrypt-attachment"); @@ -347,14 +348,14 @@ class ContentMessages { }, function(err) { error = err; if (!upload.canceled) { - var desc = "The file '"+upload.fileName+"' failed to upload."; + var desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.'; if (err.http_status == 413) { - desc = "The file '"+upload.fileName+"' exceeds this home server's size limit for uploads"; + desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName}); } var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Upload Failed", - description: desc + title: _t('Upload Failed'), + description: desc, }); } }).finally(() => { diff --git a/src/DateUtils.js b/src/DateUtils.js index 07bab4ae7b..0bce7c8a16 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -1,5 +1,6 @@ /* Copyright 2015, 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. @@ -15,38 +16,89 @@ limitations under the License. */ 'use strict'; +import { _t } from './languageHandler'; -var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; -var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +function getDaysArray() { + return [ + _t('Sun'), + _t('Mon'), + _t('Tue'), + _t('Wed'), + _t('Thu'), + _t('Fri'), + _t('Sat'), + ]; +} + +function getMonthsArray() { + return [ + _t('Jan'), + _t('Feb'), + _t('Mar'), + _t('Apr'), + _t('May'), + _t('Jun'), + _t('Jul'), + _t('Aug'), + _t('Sep'), + _t('Oct'), + _t('Nov'), + _t('Dec'), + ]; +} + +function pad(n) { + return (n < 10 ? '0' : '') + n; +} + +function twelveHourTime(date) { + let hours = date.getHours() % 12; + const minutes = pad(date.getMinutes()); + const ampm = date.getHours() >= 12 ? 'PM' : 'AM'; + hours = pad(hours ? hours : 12); + return `${hours}:${minutes}${ampm}`; +} module.exports = { formatDate: function(date) { - // date.toLocaleTimeString is completely system dependent. - // just go 24h for now - function pad(n) { - return (n < 10 ? '0' : '') + n; - } - var now = new Date(); + const days = getDaysArray(); + const months = getMonthsArray(); if (date.toDateString() === now.toDateString()) { - return pad(date.getHours()) + ':' + pad(date.getMinutes()); + return this.formatTime(date); } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { - return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + // TODO: use standard date localize function provided in counterpart + return _t('%(weekDayName)s %(time)s', {weekDayName: days[date.getDay()], time: this.formatTime(date)}); } - else /* if (now.getFullYear() === date.getFullYear()) */ { - return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + else if (now.getFullYear() === date.getFullYear()) { + // TODO: use standard date localize function provided in counterpart + return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + time: this.formatTime(date), + }); } - /* - else { - return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); - } - */ + return this.formatFullDate(date); }, - formatTime: function(date) { - //return pad(date.getHours()) + ':' + pad(date.getMinutes()); - return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2); - } -}; + formatFullDate: function(date, showTwelveHour=false) { + const days = getDaysArray(); + const months = getMonthsArray(); + return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + fullYear: date.getFullYear(), + time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date), + }); + }, + formatTime: function(date, showTwelveHour=false) { + if (showTwelveHour) { + return twelveHourTime(date); + } + return pad(date.getHours()) + ':' + pad(date.getMinutes()); + }, +}; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index a8e20f5ec1..aec32092ed 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -25,6 +25,9 @@ import emojione from 'emojione'; import classNames from 'classnames'; emojione.imagePathSVG = 'emojione/svg/'; +// Store PNG path for displaying many flags at once (for increased performance over SVG) +emojione.imagePathPNG = 'emojione/png/'; +// Use SVGs for emojis emojione.imageType = 'svg'; const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); @@ -64,16 +67,23 @@ export function unicodeToImage(str) { * emoji. * * @param alt {string} String to use for the image alt text + * @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used. * @param unicode {integer} One or more integers representing unicode characters * @returns A img node with the corresponding emoji */ -export function charactersToImageNode(alt, ...unicode) { +export function charactersToImageNode(alt, useSvg, ...unicode) { const fileName = unicode.map((u) => { return u.toString(16); }).join('-'); - return {alt}; + const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG; + const fileType = useSvg ? 'svg' : 'png'; + return {alt}; } + export function stripParagraphs(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; @@ -101,8 +111,7 @@ var sanitizeHtmlParams = { allowedTags: [ 'font', // custom to matrix for IRC-style font coloring 'del', // for markdown - // deliberately no h1/h2 to stop people shouting. - 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', ], @@ -139,17 +148,18 @@ var sanitizeHtmlParams = { attribs.href = m[1]; delete attribs.target; } - - m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); - if (m) { - var entity = m[1]; - if (entity[0] === '@') { - attribs.href = '#/user/' + entity; + else { + m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); + if (m) { + var entity = m[1]; + if (entity[0] === '@') { + attribs.href = '#/user/' + entity; + } + else if (entity[0] === '#' || entity[0] === '!') { + attribs.href = '#/room/' + entity; + } + delete attribs.target; } - else if (entity[0] === '#' || entity[0] === '!') { - attribs.href = '#/room/' + entity; - } - delete attribs.target; } } attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ @@ -335,6 +345,7 @@ export function bodyToHtml(content, highlights, opts) { } safeBody = sanitizeHtml(body, sanitizeHtmlParams); safeBody = unicodeToImage(safeBody); + safeBody = addCodeCopyButton(safeBody); } finally { delete sanitizeHtmlParams.textFilter; @@ -350,7 +361,24 @@ export function bodyToHtml(content, highlights, opts) { 'mx_EventTile_bigEmoji': emojiBody, 'markdown-body': isHtml, }); - return ; + return ; +} + +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) { diff --git a/src/KeyCode.js b/src/KeyCode.js index c9cac01239..28aafc00cb 100644 --- a/src/KeyCode.js +++ b/src/KeyCode.js @@ -32,4 +32,5 @@ module.exports = { DELETE: 46, KEY_D: 68, KEY_E: 69, + KEY_M: 77, }; diff --git a/src/Lifecycle.js b/src/Lifecycle.js index f20716cae6..54014a0166 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -19,6 +19,7 @@ import q from 'q'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; +import Analytics from './Analytics'; import Notifier from './Notifier'; import UserActivity from './UserActivity'; import Presence from './Presence'; @@ -27,6 +28,7 @@ import DMRoomMap from './utils/DMRoomMap'; import RtsClient from './RtsClient'; import Modal from './Modal'; import sdk from './index'; +import { _t } from './languageHandler'; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -49,7 +51,7 @@ import sdk from './index'; * If any of steps 1-4 are successful, it will call {setLoggedIn}, which in * turn will raise on_logged_in and will_start_client events. * - * It returns a promise which resolves when the above process completes. + * @param {object} opts * * @param {object} opts.realQueryParams: string->string map of the * query-parameters extracted from the real query-string of the starting @@ -67,6 +69,7 @@ import sdk from './index'; * @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is * true; defines the IS to use. * + * @returns {Promise} a promise which resolves when the above process completes. */ export function loadSession(opts) { const realQueryParams = opts.realQueryParams || {}; @@ -127,7 +130,7 @@ export function loadSession(opts) { function _loginWithToken(queryParams, defaultDeviceDisplayName) { // create a temporary MatrixClient to do the login - var client = Matrix.createClient({ + const client = Matrix.createClient({ baseUrl: queryParams.homeserver, }); @@ -159,7 +162,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { // Not really sure where the right home for it is. // create a temporary MatrixClient to do the login - var client = Matrix.createClient({ + const client = Matrix.createClient({ baseUrl: hsUrl, }); @@ -184,34 +187,42 @@ 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); } - const hs_url = localStorage.getItem("mx_hs_url"); - const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; - const access_token = localStorage.getItem("mx_access_token"); - const user_id = localStorage.getItem("mx_user_id"); - const device_id = localStorage.getItem("mx_device_id"); + const hsUrl = localStorage.getItem("mx_hs_url"); + const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org'; + const accessToken = localStorage.getItem("mx_access_token"); + const userId = localStorage.getItem("mx_user_id"); + const deviceId = localStorage.getItem("mx_device_id"); - let is_guest; + let isGuest; if (localStorage.getItem("mx_is_guest") !== null) { - is_guest = localStorage.getItem("mx_is_guest") === "true"; + isGuest = localStorage.getItem("mx_is_guest") === "true"; } else { // legacy key name - is_guest = localStorage.getItem("matrix-is-guest") === "true"; + isGuest = localStorage.getItem("matrix-is-guest") === "true"; } - if (access_token && user_id && hs_url) { - console.log("Restoring session for %s", user_id); + if (accessToken && userId && hsUrl) { + console.log("Restoring session for %s", userId); try { setLoggedIn({ - userId: user_id, - deviceId: device_id, - accessToken: access_token, - homeserverUrl: hs_url, - identityServerUrl: is_url, - guest: is_guest, + userId: userId, + deviceId: deviceId, + accessToken: accessToken, + homeserverUrl: hsUrl, + identityServerUrl: isUrl, + guest: isGuest, }); return q(true); } catch (e) { @@ -228,14 +239,16 @@ function _handleRestoreFailure(e) { let msg = e.message; if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") { - msg = "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."; + 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.', + ); _clearLocalStorage(); return q.reject(new Error( - "Unable to restore previous session: " + msg, + _t('Unable to restore previous session') + ': ' + msg, )); } @@ -273,9 +286,15 @@ export function initRtsClient(url) { */ export function setLoggedIn(credentials) { credentials.guest = Boolean(credentials.guest); - console.log("setLoggedIn => %s (guest=%s) hs=%s", - credentials.userId, credentials.guest, - credentials.homeserverUrl); + + Analytics.setGuest(credentials.guest); + + console.log( + "setLoggedIn: mxid:", credentials.userId, + "deviceId:", credentials.deviceId, + "guest:", credentials.guest, + "hs:", credentials.homeserverUrl, + ); // This is dispatched to indicate that the user is still in the process of logging in // because `teamPromise` may take some time to resolve, breaking the assumption that // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms @@ -303,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); @@ -352,7 +381,7 @@ export function logout() { return; } - return MatrixClientPeg.get().logout().then(onLoggedOut, + MatrixClientPeg.get().logout().then(onLoggedOut, (err) => { // Just throwing an error here is going to be very unhelpful // if you're trying to log out because your server's down and @@ -363,8 +392,8 @@ export function logout() { // change your password). console.log("Failed to call logout API: token will not be invalidated"); onLoggedOut(); - } - ); + }, + ).done(); } /** @@ -397,6 +426,7 @@ export function onLoggedOut() { } function _clearLocalStorage() { + Analytics.logout(); if (!window.localStorage) { return; } @@ -420,7 +450,7 @@ export function stopMatrixClient() { UserActivity.stop(); Presence.stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); - var cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.get(); if (cli) { cli.stopClient(); cli.removeAllListeners(); diff --git a/src/Login.js b/src/Login.js index 107a8825e9..87731744e9 100644 --- a/src/Login.js +++ b/src/Login.js @@ -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'; @@ -97,9 +98,9 @@ export default class Login { }; }, (error) => { if (error.httpStatus === 403) { - error.friendlyText = "Guest access is disabled on this Home Server."; + error.friendlyText = _t("Guest access is disabled on this Home Server."); } else { - error.friendlyText = "Failed to register as guest: " + error.data; + error.friendlyText = _t("Failed to register as guest:") + ' ' + error.data; } throw error; }); @@ -158,12 +159,12 @@ export default class Login { }, function(error) { if (error.httpStatus == 400 && loginParams.medium) { error.friendlyText = ( - 'This Home Server does not support login using email address.' + _t('This Home Server does not support login using email address.') ); } else if (error.httpStatus === 403) { error.friendlyText = ( - 'Incorrect username and/or password.' + _t('Incorrect username and/or password.') ); if (self._fallbackHsUrl) { var fbClient = Matrix.createClient({ @@ -187,7 +188,7 @@ export default class Login { } else { error.friendlyText = ( - 'There was a problem logging in. (HTTP ' + error.httpStatus + ")" + _t("There was a problem logging in.") + ' (HTTP ' + error.httpStatus + ")" ); } throw error; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 452b67c4ee..94e55a8d8a 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -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'; diff --git a/src/Modal.js b/src/Modal.js index 7be37da92e..8d53b2da7d 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -19,6 +19,7 @@ limitations under the License. var React = require('react'); var ReactDOM = require('react-dom'); +import Analytics from './Analytics'; import sdk from './index'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; @@ -104,6 +105,9 @@ class ModalManager { } createDialog(Element, props, className) { + if (props && props.title) { + Analytics.trackEvent('Modal', props.title, 'createDialog'); + } return this.createDialogAsync((cb) => {cb(Element);}, props, className); } diff --git a/src/Notifier.js b/src/Notifier.js index 92770877b7..40a65d4106 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -15,11 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -var MatrixClientPeg = require("./MatrixClientPeg"); -var PlatformPeg = require("./PlatformPeg"); -var TextForEvent = require('./TextForEvent'); -var Avatar = require('./Avatar'); -var dis = require("./dispatcher"); +import MatrixClientPeg from './MatrixClientPeg'; +import PlatformPeg from './PlatformPeg'; +import TextForEvent from './TextForEvent'; +import Analytics from './Analytics'; +import Avatar from './Avatar'; +import dis from './dispatcher'; +import sdk from './index'; +import { _t } from './languageHandler'; +import Modal from './Modal'; /* * Dispatches: @@ -29,7 +33,7 @@ var dis = require("./dispatcher"); * } */ -var Notifier = { +const Notifier = { notifsByRoom: {}, notificationMessageForEvent: function(ev) { @@ -48,16 +52,16 @@ var Notifier = { return; } - var msg = this.notificationMessageForEvent(ev); + let msg = this.notificationMessageForEvent(ev); if (!msg) return; - var title; - if (!ev.sender || room.name == ev.sender.name) { + let title; + if (!ev.sender || room.name === ev.sender.name) { title = room.name; // notificationMessageForEvent includes sender, // but we already have the sender here if (ev.getContent().body) msg = ev.getContent().body; - } else if (ev.getType() == 'm.room.member') { + } else if (ev.getType() === 'm.room.member') { // context is all in the message here, we don't need // to display sender info title = room.name; @@ -68,7 +72,7 @@ var Notifier = { if (ev.getContent().body) msg = ev.getContent().body; } - var avatarUrl = ev.sender ? Avatar.avatarUrlForMember( + const avatarUrl = ev.sender ? Avatar.avatarUrlForMember( ev.sender, 40, 40, 'crop' ) : null; @@ -83,7 +87,7 @@ var Notifier = { }, _playAudioNotification: function(ev, room) { - var e = document.getElementById("messageAudio"); + const e = document.getElementById("messageAudio"); if (e) { e.load(); e.play(); @@ -95,7 +99,7 @@ var Notifier = { this.boundOnSyncStateChange = this.onSyncStateChange.bind(this); this.boundOnRoomReceipt = this.onRoomReceipt.bind(this); MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline); - MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt); + MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt); MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); this.toolbarHidden = false; this.isSyncing = false; @@ -104,7 +108,7 @@ var Notifier = { stop: function() { if (MatrixClientPeg.get() && this.boundOnRoomTimeline) { MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline); - MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt); + MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt); MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); } this.isSyncing = false; @@ -118,10 +122,13 @@ var Notifier = { setEnabled: function(enable, callback) { const plaf = PlatformPeg.get(); if (!plaf) return; + + Analytics.trackEvent('Notifier', 'Set Enabled', enable); + // make sure that we persist the current setting audio_enabled setting // before changing anything if (global.localStorage) { - if(global.localStorage.getItem('audio_notifications_enabled') == null) { + if (global.localStorage.getItem('audio_notifications_enabled') === null) { this.setAudioEnabled(this.isEnabled()); } } @@ -131,6 +138,14 @@ var Notifier = { plaf.requestNotificationPermission().done((result) => { if (result !== 'granted') { // The permission request was dismissed or denied + const description = result === 'denied' + ? _t('Riot does not have permission to send you notifications - please check your browser settings') + : _t('Riot was not given permission to send notifications - please try again'); + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createDialog(ErrorDialog, { + title: _t('Unable to enable Notifications'), + description, + }); return; } @@ -141,7 +156,7 @@ var Notifier = { if (callback) callback(); dis.dispatch({ action: "notifier_enabled", - value: true + value: true, }); }); // clear the notifications_hidden flag, so that if notifications are @@ -152,7 +167,7 @@ var Notifier = { global.localStorage.setItem('notifications_enabled', 'false'); dis.dispatch({ action: "notifier_enabled", - value: false + value: false, }); } }, @@ -165,7 +180,7 @@ var Notifier = { if (!global.localStorage) return true; - var enabled = global.localStorage.getItem('notifications_enabled'); + const enabled = global.localStorage.getItem('notifications_enabled'); if (enabled === null) return true; return enabled === 'true'; }, @@ -173,12 +188,12 @@ var Notifier = { setAudioEnabled: function(enable) { if (!global.localStorage) return; global.localStorage.setItem('audio_notifications_enabled', - enable ? 'true' : 'false'); + enable ? 'true' : 'false'); }, isAudioEnabled: function(enable) { if (!global.localStorage) return true; - var enabled = global.localStorage.getItem( + const enabled = global.localStorage.getItem( 'audio_notifications_enabled'); // default to true if the popups are enabled if (enabled === null) return this.isEnabled(); @@ -188,11 +203,13 @@ var Notifier = { setToolbarHidden: function(hidden, persistent = true) { this.toolbarHidden = hidden; + Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); + // XXX: why are we dispatching this here? // this is nothing to do with notifier_enabled dis.dispatch({ action: "notifier_enabled", - value: this.isEnabled() + value: this.isEnabled(), }); // update the info to localStorage for persistent settings @@ -215,8 +232,7 @@ var Notifier = { onSyncStateChange: function(state) { if (state === "SYNCING") { this.isSyncing = true; - } - else if (state === "STOPPED" || state === "ERROR") { + } else if (state === "STOPPED" || state === "ERROR") { this.isSyncing = false; } }, @@ -225,22 +241,23 @@ var Notifier = { if (toStartOfTimeline) return; if (!room) return; if (!this.isSyncing) return; // don't alert for any messages initially - if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return; + if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; - var actions = MatrixClientPeg.get().getPushActionsForEvent(ev); + const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.notify) { if (this.isEnabled()) { this._displayPopupNotification(ev, room); } if (actions.tweaks.sound && this.isAudioEnabled()) { + PlatformPeg.get().loudNotification(ev, room); this._playAudioNotification(ev, room); } } }, onRoomReceipt: function(ev, room) { - if (room.getUnreadNotificationCount() == 0) { + if (room.getUnreadNotificationCount() === 0) { // ideally we would clear each notification when it was read, // but we have no way, given a read receipt, to know whether // the receipt comes before or after an event, so we can't @@ -255,7 +272,7 @@ var Notifier = { } delete this.notifsByRoom[room.roomId]; } - } + }, }; if (!global.mxNotifier) { diff --git a/src/PasswordReset.js b/src/PasswordReset.js index a03a565459..0739ca0a24 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -15,6 +15,7 @@ limitations under the License. */ var Matrix = require("matrix-js-sdk"); +import { _t } from './languageHandler'; /** * Allows a user to reset their password on a homeserver. @@ -53,7 +54,7 @@ class PasswordReset { return res; }, function(err) { if (err.errcode == 'M_THREEPID_NOT_FOUND') { - err.message = "This email address was not found"; + err.message = _t('This email address was not found'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } @@ -78,10 +79,10 @@ class PasswordReset { } }, this.password).catch(function(err) { if (err.httpStatus === 401) { - err.message = "Failed to verify email address: make sure you clicked the link in the email"; + err.message = _t('Failed to verify email address: make sure you clicked the link in the email'); } else if (err.httpStatus === 404) { - err.message = "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})`; diff --git a/src/Roles.js b/src/Roles.js new file mode 100644 index 0000000000..8c1f711bbe --- /dev/null +++ b/src/Roles.js @@ -0,0 +1,34 @@ +/* +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 { _t } from './languageHandler'; + +export function levelRoleMap() { + return { + undefined: _t('Default'), + 0: _t('User'), + 50: _t('Moderator'), + 100: _t('Admin'), + }; +} + +export function textualPowerLevel(level, userDefault) { + const LEVEL_ROLE_MAP = this.levelRoleMap(); + if (LEVEL_ROLE_MAP[level]) { + return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); + } else { + return level; + } +} diff --git a/src/Rooms.js b/src/Rooms.js index 08fa7f797f..16b5ab9ee2 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -15,7 +15,6 @@ limitations under the License. */ import MatrixClientPeg from './MatrixClientPeg'; -import DMRoomMap from './utils/DMRoomMap'; import q from 'q'; /** diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index dbb7e405df..c1b975e8e8 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -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. @@ -125,6 +141,7 @@ const SdkConfig = require('./SdkConfig'); const MatrixClientPeg = require("./MatrixClientPeg"); const MatrixEvent = require("matrix-js-sdk").MatrixEvent; const dis = require("./dispatcher"); +import { _t } from './languageHandler'; function sendResponse(event, res) { const data = JSON.parse(JSON.stringify(event.data)); @@ -150,7 +167,7 @@ function inviteUser(event, roomId, userId) { console.log(`Received request to invite ${userId} into room ${roomId}`); const client = MatrixClientPeg.get(); if (!client) { - sendError(event, "You need to be logged in."); + sendError(event, _t('You need to be logged in.')); return; } const room = client.getRoom(roomId); @@ -170,7 +187,7 @@ function inviteUser(event, roomId, userId) { success: true, }); }, function(err) { - sendError(event, "You need to be able to invite users to do that.", err); + sendError(event, _t('You need to be able to invite users to do that.'), err); }); } @@ -181,7 +198,7 @@ function setPlumbingState(event, roomId, status) { console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`); const client = MatrixClientPeg.get(); if (!client) { - sendError(event, "You need to be logged in."); + sendError(event, _t('You need to be logged in.')); return; } client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => { @@ -189,7 +206,7 @@ function setPlumbingState(event, roomId, status) { success: true, }); }, (err) => { - sendError(event, err.message ? err.message : "Failed to send request.", err); + sendError(event, err.message ? err.message : _t('Failed to send request.'), err); }); } @@ -197,7 +214,7 @@ function setBotOptions(event, roomId, userId) { console.log(`Received request to set options for bot ${userId} in room ${roomId}`); const client = MatrixClientPeg.get(); if (!client) { - sendError(event, "You need to be logged in."); + sendError(event, _t('You need to be logged in.')); return; } client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { @@ -205,20 +222,20 @@ function setBotOptions(event, roomId, userId) { success: true, }); }, (err) => { - sendError(event, err.message ? err.message : "Failed to send request.", err); + sendError(event, err.message ? err.message : _t('Failed to send request.'), err); }); } function setBotPower(event, roomId, userId, level) { if (!(Number.isInteger(level) && level >= 0)) { - sendError(event, "Power level must be positive integer."); + sendError(event, _t('Power level must be positive integer.')); return; } console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`); const client = MatrixClientPeg.get(); if (!client) { - sendError(event, "You need to be logged in."); + sendError(event, _t('You need to be logged in.')); return; } @@ -235,7 +252,7 @@ function setBotPower(event, roomId, userId, level) { success: true, }); }, (err) => { - sendError(event, err.message ? err.message : "Failed to send request.", err); + sendError(event, err.message ? err.message : _t('Failed to send request.'), err); }); }); } @@ -255,15 +272,30 @@ function botOptions(event, roomId, userId) { returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId); } -function returnStateEvent(event, roomId, eventType, stateKey) { +function getMembershipCount(event, roomId) { const client = MatrixClientPeg.get(); if (!client) { - sendError(event, "You need to be logged in."); + sendError(event, _t('You need to be logged in.')); return; } const room = client.getRoom(roomId); if (!room) { - sendError(event, "This room is not recognised."); + 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) { + 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 stateEvent = room.currentState.getStateEvents(eventType, stateKey); @@ -313,13 +345,13 @@ const onMessage = function(event) { const roomId = event.data.room_id; const userId = event.data.user_id; if (!roomId) { - sendError(event, "Missing room_id in request"); + sendError(event, _t('Missing room_id in request')); return; } let promise = Promise.resolve(currentRoomId); if (!currentRoomId) { if (!currentRoomAlias) { - sendError(event, "Must be viewing a room"); + sendError(event, _t('Must be viewing a room')); return; } // no room ID but there is an alias, look it up. @@ -331,7 +363,7 @@ const onMessage = function(event) { promise.then((viewingRoomId) => { if (roomId !== viewingRoomId) { - sendError(event, "Room " + roomId + " not visible"); + sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId})); return; } @@ -342,10 +374,13 @@ 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) { - sendError(event, "Missing user_id in request"); + sendError(event, _t('Missing user_id in request')); return; } switch (event.data.action) { @@ -370,7 +405,7 @@ const onMessage = function(event) { } }, (err) => { console.error(err); - sendError(event, "Failed to lookup current room."); + sendError(event, _t('Failed to lookup current room') + '.'); }); }; diff --git a/src/Skinner.js b/src/Skinner.js index 4482f2239c..0688c9fc26 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -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) { diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 1ddcf4832d..185ea504ac 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -14,10 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -var MatrixClientPeg = require("./MatrixClientPeg"); -var dis = require("./dispatcher"); -var Tinter = require("./Tinter"); +import MatrixClientPeg from "./MatrixClientPeg"; +import dis from "./dispatcher"; +import Tinter from "./Tinter"; import sdk from './index'; +import { _t } from './languageHandler'; import Modal from './Modal'; @@ -41,58 +42,64 @@ class Command { } getUsage() { - return "Usage: " + this.getCommandWithArgs(); + return _t('Usage') + ': ' + this.getCommandWithArgs(); } } -var reject = function(msg) { +function reject(msg) { return { - error: msg + error: msg, }; -}; +} -var success = function(promise) { +function success(promise) { return { - promise: promise + promise: promise, }; -}; +} -var commands = { +/* Disable the "unexpected this" error for these commands - all of the run + * functions are called with `this` bound to the Command instance. + */ + +/* eslint-disable babel/no-invalid-this */ + +const commands = { ddg: new Command("ddg", "", function(roomId, args) { const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); // TODO Don't explain this away, actually show a search UI here. Modal.createDialog(ErrorDialog, { - title: "/ddg is not a command", - description: "To use it, just wait for autocomplete results to load and tab through them.", + title: _t('/ddg is not a command'), + description: _t('To use it, just wait for autocomplete results to load and tab through them.'), }); return success(); }), // Change your nickname - nick: new Command("nick", "", function(room_id, args) { + nick: new Command("nick", "", function(roomId, args) { if (args) { return success( - MatrixClientPeg.get().setDisplayName(args) + MatrixClientPeg.get().setDisplayName(args), ); } return reject(this.getUsage()); }), // Changes the colorscheme of your current room - tint: new Command("tint", " []", function(room_id, args) { + tint: new Command("tint", " []", function(roomId, args) { if (args) { - var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); + const matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); if (matches) { Tinter.tint(matches[1], matches[4]); - var colorScheme = {}; + const colorScheme = {}; colorScheme.primary_color = matches[1]; if (matches[4]) { colorScheme.secondary_color = matches[4]; } return success( MatrixClientPeg.get().setRoomAccountData( - room_id, "org.matrix.room.color_scheme", colorScheme - ) + roomId, "org.matrix.room.color_scheme", colorScheme, + ), ); } } @@ -100,22 +107,22 @@ var commands = { }), // Change the room topic - topic: new Command("topic", "", function(room_id, args) { + topic: new Command("topic", "", function(roomId, args) { if (args) { return success( - MatrixClientPeg.get().setRoomTopic(room_id, args) + MatrixClientPeg.get().setRoomTopic(roomId, args), ); } return reject(this.getUsage()); }), // Invite a user - invite: new Command("invite", "", function(room_id, args) { + invite: new Command("invite", "", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+)$/); + const matches = args.match(/^(\S+)$/); if (matches) { return success( - MatrixClientPeg.get().invite(room_id, matches[1]) + MatrixClientPeg.get().invite(roomId, matches[1]), ); } } @@ -123,21 +130,21 @@ var commands = { }), // Join a room - join: new Command("join", "#alias:domain", function(room_id, args) { + join: new Command("join", "#alias:domain", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+)$/); + const matches = args.match(/^(\S+)$/); if (matches) { - var room_alias = matches[1]; - if (room_alias[0] !== '#') { + let roomAlias = matches[1]; + if (roomAlias[0] !== '#') { return reject(this.getUsage()); } - if (!room_alias.match(/:/)) { - room_alias += ':' + MatrixClientPeg.get().getDomain(); + if (!roomAlias.match(/:/)) { + roomAlias += ':' + MatrixClientPeg.get().getDomain(); } dis.dispatch({ action: 'view_room', - room_alias: room_alias, + room_alias: roomAlias, auto_join: true, }); @@ -147,29 +154,29 @@ var commands = { return reject(this.getUsage()); }), - part: new Command("part", "[#alias:domain]", function(room_id, args) { - var targetRoomId; + part: new Command("part", "[#alias:domain]", function(roomId, args) { + let targetRoomId; if (args) { - var matches = args.match(/^(\S+)$/); + const matches = args.match(/^(\S+)$/); if (matches) { - var room_alias = matches[1]; - if (room_alias[0] !== '#') { + let roomAlias = matches[1]; + if (roomAlias[0] !== '#') { return reject(this.getUsage()); } - if (!room_alias.match(/:/)) { - room_alias += ':' + MatrixClientPeg.get().getDomain(); + if (!roomAlias.match(/:/)) { + roomAlias += ':' + MatrixClientPeg.get().getDomain(); } // Try to find a room with this alias - var rooms = MatrixClientPeg.get().getRooms(); - for (var i = 0; i < rooms.length; i++) { - var aliasEvents = rooms[i].currentState.getStateEvents( - "m.room.aliases" + const rooms = MatrixClientPeg.get().getRooms(); + for (let i = 0; i < rooms.length; i++) { + const aliasEvents = rooms[i].currentState.getStateEvents( + "m.room.aliases", ); - for (var j = 0; j < aliasEvents.length; j++) { - var aliases = aliasEvents[j].getContent().aliases || []; - for (var k = 0; k < aliases.length; k++) { - if (aliases[k] === room_alias) { + for (let j = 0; j < aliasEvents.length; j++) { + const aliases = aliasEvents[j].getContent().aliases || []; + for (let k = 0; k < aliases.length; k++) { + if (aliases[k] === roomAlias) { targetRoomId = rooms[i].roomId; break; } @@ -178,27 +185,28 @@ var commands = { } if (targetRoomId) { break; } } - } - if (!targetRoomId) { - return reject("Unrecognised room alias: " + room_alias); + if (!targetRoomId) { + return reject(_t("Unrecognised room alias:") + ' ' + roomAlias); + } } } - if (!targetRoomId) targetRoomId = room_id; + if (!targetRoomId) targetRoomId = roomId; return success( MatrixClientPeg.get().leave(targetRoomId).then( - function() { - dis.dispatch({action: 'view_next_room'}); - }) + function() { + dis.dispatch({action: 'view_next_room'}); + }, + ), ); }), // Kick a user from the room with an optional reason - kick: new Command("kick", " []", function(room_id, args) { + kick: new Command("kick", " []", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+?)( +(.*))?$/); + const matches = args.match(/^(\S+?)( +(.*))?$/); if (matches) { return success( - MatrixClientPeg.get().kick(room_id, matches[1], matches[3]) + MatrixClientPeg.get().kick(roomId, matches[1], matches[3]), ); } } @@ -206,12 +214,12 @@ var commands = { }), // Ban a user from the room with an optional reason - ban: new Command("ban", " []", function(room_id, args) { + ban: new Command("ban", " []", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+?)( +(.*))?$/); + const matches = args.match(/^(\S+?)( +(.*))?$/); if (matches) { return success( - MatrixClientPeg.get().ban(room_id, matches[1], matches[3]) + MatrixClientPeg.get().ban(roomId, matches[1], matches[3]), ); } } @@ -219,13 +227,13 @@ var commands = { }), // Unban a user from the room - unban: new Command("unban", "", function(room_id, args) { + unban: new Command("unban", "", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+)$/); + const matches = args.match(/^(\S+)$/); if (matches) { // Reset the user membership to "leave" to unban him return success( - MatrixClientPeg.get().unban(room_id, matches[1]) + MatrixClientPeg.get().unban(roomId, matches[1]), ); } } @@ -233,27 +241,27 @@ var commands = { }), // Define the power level of a user - op: new Command("op", " []", function(room_id, args) { + op: new Command("op", " []", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+?)( +(\d+))?$/); - var powerLevel = 50; // default power level for op + const matches = args.match(/^(\S+?)( +(\d+))?$/); + let powerLevel = 50; // default power level for op if (matches) { - var user_id = matches[1]; + const userId = matches[1]; if (matches.length === 4 && undefined !== matches[3]) { powerLevel = parseInt(matches[3]); } - if (powerLevel !== NaN) { - var room = MatrixClientPeg.get().getRoom(room_id); + if (!isNaN(powerLevel)) { + const room = MatrixClientPeg.get().getRoom(roomId); if (!room) { - return reject("Bad room ID: " + room_id); + return reject("Bad room ID: " + roomId); } - var powerLevelEvent = room.currentState.getStateEvents( - "m.room.power_levels", "" + const powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "", ); return success( MatrixClientPeg.get().setPowerLevel( - room_id, user_id, powerLevel, powerLevelEvent - ) + roomId, userId, powerLevel, powerLevelEvent, + ), ); } } @@ -262,32 +270,94 @@ var commands = { }), // Reset the power level of a user - deop: new Command("deop", "", function(room_id, args) { + deop: new Command("deop", "", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+)$/); + const matches = args.match(/^(\S+)$/); if (matches) { - var room = MatrixClientPeg.get().getRoom(room_id); + const room = MatrixClientPeg.get().getRoom(roomId); if (!room) { - return reject("Bad room ID: " + room_id); + return reject("Bad room ID: " + roomId); } - var powerLevelEvent = room.currentState.getStateEvents( - "m.room.power_levels", "" + const powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "", ); return success( MatrixClientPeg.get().setPowerLevel( - room_id, args, undefined, powerLevelEvent - ) + roomId, args, undefined, powerLevelEvent, + ), ); } } return reject(this.getUsage()); - }) + }), + + // Verify a user, device, and pubkey tuple + verify: new Command("verify", " ", function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); + if (matches) { + const userId = matches[1]; + const deviceId = matches[2]; + const fingerprint = matches[3]; + + const device = MatrixClientPeg.get().getStoredDevice(userId, deviceId); + if (!device) { + return reject(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`); + } + + if (device.isVerified()) { + if (device.getFingerprint() === fingerprint) { + return reject(_t(`Device already verified!`)); + } else { + return reject(_t(`WARNING: Device already verified, but keys do NOT MATCH!`)); + } + } + + if (device.getFingerprint() === fingerprint) { + MatrixClientPeg.get().setDeviceVerified( + userId, deviceId, true, + ); + + // Tell the user we verified everything! + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createDialog(QuestionDialog, { + title: _t("Verified key"), + description: ( +
+

+ { + _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}) + } +

+
+ ), + hasCancelButton: false, + }); + + return success(); + } else { + 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}) + ); + } + } + } + return reject(this.getUsage()); + }), }; +/* eslint-enable babel/no-invalid-this */ + // helpful aliases -var aliases = { - j: "join" +const aliases = { + j: "join", }; module.exports = { @@ -304,13 +374,13 @@ module.exports = { // IRC-style commands input = input.replace(/\s+$/, ""); if (input[0] === "/" && input[1] !== "/") { - var bits = input.match(/^(\S+?)( +((.|\n)*))?$/); - var cmd, args; + const bits = input.match(/^(\S+?)( +((.|\n)*))?$/); + let cmd; + let args; if (bits) { cmd = bits[1].substring(1).toLowerCase(); args = bits[3]; - } - else { + } else { cmd = input; } if (cmd === "me") return null; @@ -319,9 +389,8 @@ module.exports = { } if (commands[cmd]) { return commands[cmd].run(roomId, args); - } - else { - return reject("Unrecognised command: " + input); + } else { + return reject(_t("Unrecognised command:") + ' ' + input); } } return null; // not a command @@ -329,12 +398,12 @@ module.exports = { getCommandList: function() { // Return all the commands plus /me and /markdown which aren't handled like normal commands - var cmds = Object.keys(commands).sort().map(function(cmdKey) { + const cmds = Object.keys(commands).sort().map(function(cmdKey) { return commands[cmdKey]; }); cmds.push(new Command("me", "", function() {})); cmds.push(new Command("markdown", "", function() {})); return cmds; - } + }, }; diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 3e1659f392..fa78f9d61b 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -16,6 +16,8 @@ limitations under the License. var MatrixClientPeg = require("./MatrixClientPeg"); var CallHandler = require("./CallHandler"); +import { _t } from './languageHandler'; +import * as Roles from './Roles'; function textForMemberEvent(ev) { // XXX: SYJS-16 "sender is sometimes null for join messages" @@ -23,95 +25,103 @@ function textForMemberEvent(ev) { var targetName = ev.target ? ev.target.name : ev.getStateKey(); var ConferenceHandler = CallHandler.getConferenceHandler(); var reason = ev.getContent().reason ? ( - " Reason: " + ev.getContent().reason + _t('Reason') + ': ' + ev.getContent().reason ) : ""; switch (ev.getContent().membership) { case 'invite': var threePidContent = ev.getContent().third_party_invite; if (threePidContent) { if (threePidContent.display_name) { - return targetName + " accepted the invitation for " + - threePidContent.display_name + "."; + return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name}); } else { - return targetName + " accepted an invitation."; + return _t('%(targetName)s accepted an invitation.', {targetName: targetName}); } } else { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { - return senderName + " requested a VoIP conference"; + return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName}); } else { - return senderName + " invited " + targetName + "."; + return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName}); } } case 'ban': - return senderName + " banned " + targetName + "." + reason; + return _t( + '%(senderName)s banned %(targetName)s.', + {senderName: senderName, targetName: targetName} + ) + ' ' + reason; case 'join': if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') { if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) { - return ev.getSender() + " changed their display name from " + - ev.getPrevContent().displayname + " to " + - ev.getContent().displayname; + return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname}); } else if (!ev.getPrevContent().displayname && ev.getContent().displayname) { - return ev.getSender() + " set their display name to " + ev.getContent().displayname; + return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname}); } else if (ev.getPrevContent().displayname && !ev.getContent().displayname) { - return ev.getSender() + " removed their display name (" + ev.getPrevContent().displayname + ")"; + return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname}); } else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) { - return senderName + " removed their profile picture"; + return _t('%(senderName)s removed their profile picture.', {senderName: senderName}); } else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) { - return senderName + " changed their profile picture"; + return _t('%(senderName)s changed their profile picture.', {senderName: senderName}); } else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) { - return senderName + " set a profile picture"; + return _t('%(senderName)s set a profile picture.', {senderName: senderName}); } else { - // hacky hack for https://github.com/vector-im/vector-web/issues/2020 - return senderName + " rejoined the room."; + // suppress null rejoins + return ''; } } else { if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { - return "VoIP conference started"; + return _t('VoIP conference started.'); } else { - return targetName + " joined the room."; + return _t('%(targetName)s joined the room.', {targetName: targetName}); } } case 'leave': if (ev.getSender() === ev.getStateKey()) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { - return "VoIP conference finished"; + return _t('VoIP conference finished.'); } else if (ev.getPrevContent().membership === "invite") { - return targetName + " rejected the invitation."; + return _t('%(targetName)s rejected the invitation.', {targetName: targetName}); } else { - return targetName + " left the room."; + return _t('%(targetName)s left the room.', {targetName: targetName}); } } else if (ev.getPrevContent().membership === "ban") { - return senderName + " unbanned " + targetName + "."; + return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName}); } else if (ev.getPrevContent().membership === "join") { - return senderName + " kicked " + targetName + "." + reason; + return _t( + '%(senderName)s kicked %(targetName)s.', + {senderName: senderName, targetName: targetName} + ) + ' ' + reason; } else if (ev.getPrevContent().membership === "invite") { - return senderName + " withdrew " + targetName + "'s invitation." + reason; + return _t( + '%(senderName)s withdrew %(targetName)s\'s invitation.', + {senderName: senderName, targetName: targetName} + ) + ' ' + reason; } else { - return targetName + " left the room."; + return _t('%(targetName)s left the room.', {targetName: targetName}); } } } function textForTopicEvent(ev) { var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - - return senderDisplayName + ' changed the topic to "' + ev.getContent().topic + '"'; + return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic}); } function textForRoomNameEvent(ev) { var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - - return senderDisplayName + ' changed the room name to "' + ev.getContent().name + '"'; + + if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { + return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName}); + } + return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name}); } function textForMessageEvent(ev) { @@ -120,66 +130,111 @@ function textForMessageEvent(ev) { if (ev.getContent().msgtype === "m.emote") { message = "* " + senderDisplayName + " " + message; } else if (ev.getContent().msgtype === "m.image") { - message = senderDisplayName + " sent an image."; + message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName}); } return message; } function textForCallAnswerEvent(event) { - var senderName = event.sender ? event.sender.name : "Someone"; - var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; - return senderName + " answered the call." + supported; + var senderName = event.sender ? event.sender.name : _t('Someone'); + var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)'); + return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported; } function textForCallHangupEvent(event) { - var senderName = event.sender ? event.sender.name : "Someone"; - var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; - return senderName + " ended the call." + supported; + 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; } function textForCallInviteEvent(event) { - var senderName = event.sender ? event.sender.name : "Someone"; + var senderName = event.sender ? event.sender.name : _t('Someone'); // FIXME: Find a better way to determine this from the event? var type = "voice"; if (event.getContent().offer && event.getContent().offer.sdp && event.getContent().offer.sdp.indexOf('m=video') !== -1) { type = "video"; } - var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; - return senderName + " placed a " + type + " call." + supported; + var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)'); + return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported; } function textForThreePidInviteEvent(event) { var senderName = event.sender ? event.sender.name : event.getSender(); - return senderName + " sent an invitation to " + event.getContent().display_name + - " to join the room."; + return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name}); } function textForHistoryVisibilityEvent(event) { var senderName = event.sender ? event.sender.name : event.getSender(); var vis = event.getContent().history_visibility; - var text = senderName + " made future room history visible to "; + // XXX: This i18n just isn't going to work for languages with different sentence structure. + var text = _t('%(senderName)s made future room history visible to', {senderName: senderName}) + ' '; if (vis === "invited") { - text += "all room members, from the point they are invited."; + text += _t('all room members, from the point they are invited') + '.'; } else if (vis === "joined") { - text += "all room members, from the point they joined."; + text += _t('all room members, from the point they joined') + '.'; } else if (vis === "shared") { - text += "all room members."; + text += _t('all room members') + '.'; } else if (vis === "world_readable") { - text += "anyone."; + text += _t('anyone') + '.'; } else { - text += " unknown (" + vis + ")"; + text += ' ' + _t('unknown') + ' (' + vis + ').'; } return text; } function textForEncryptionEvent(event) { var senderName = event.sender ? event.sender.name : event.getSender(); - return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")"; + return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {senderName: senderName, algorithm: event.getContent().algorithm}); +} + +// Currently will only display a change if a user's power level is changed +function textForPowerEvent(event) { + const senderName = event.sender ? event.sender.name : event.getSender(); + if (!event.getPrevContent() || !event.getPrevContent().users) { + return ''; + } + const userDefault = event.getContent().users_default || 0; + // Construct set of userIds + let users = []; + Object.keys(event.getContent().users).forEach( + (userId) => { + if (users.indexOf(userId) === -1) users.push(userId); + } + ); + Object.keys(event.getPrevContent().users).forEach( + (userId) => { + if (users.indexOf(userId) === -1) users.push(userId); + } + ); + let diff = []; + // XXX: This is also surely broken for i18n + users.forEach((userId) => { + // Previous power level + const from = event.getPrevContent().users[userId]; + // Current power level + const to = event.getContent().users[userId]; + if (to !== from) { + diff.push( + _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { + userId: userId, + fromPowerLevel: Roles.textualPowerLevel(from, userDefault), + toPowerLevel: Roles.textualPowerLevel(to, userDefault) + }) + ); + } + }); + if (!diff.length) { + return ''; + } + return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', { + senderName: senderName, + powerLevelDiffText: diff.join(", ") + }); } var handlers = { @@ -193,6 +248,7 @@ var handlers = { 'm.room.third_party_invite': textForThreePidInviteEvent, 'm.room.history_visibility': textForHistoryVisibilityEvent, 'm.room.encryption': textForEncryptionEvent, + 'm.room.power_levels': textForPowerEvent, }; module.exports = { diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js index 2aa0573e22..2b1cf23380 100644 --- a/src/UnknownDeviceErrorHandler.js +++ b/src/UnknownDeviceErrorHandler.js @@ -22,7 +22,7 @@ let isDialogOpen = false; const onAction = function(payload) { if (payload.action === 'unknown_device_error' && !isDialogOpen) { - var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); isDialogOpen = true; Modal.createDialog(UnknownDeviceDialog, { devices: payload.err.devices, @@ -33,17 +33,17 @@ const onAction = function(payload) { // https://github.com/vector-im/riot-web/issues/3148 console.log('UnknownDeviceDialog closed with '+r); }, - }, "mx_Dialog_unknownDevice"); + }, 'mx_Dialog_unknownDevice'); } -} +}; let ref = null; -export function startListening () { +export function startListening() { ref = dis.register(onAction); } -export function stopListening () { +export function stopListening() { if (ref) { dis.unregister(ref); ref = null; diff --git a/src/Unread.js b/src/Unread.js index d7490c8632..67166dc24f 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -25,7 +25,9 @@ module.exports = { eventTriggersUnreadCount: function(ev) { if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { return false; - } else if (ev.getType() == "m.room.member") { + } else if (ev.getType() == 'm.room.member') { + return false; + } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { return false; } else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { return false; diff --git a/src/UserActivity.js b/src/UserActivity.js index e7338e17e9..1ae272f5df 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -32,7 +32,7 @@ class UserActivity { start() { document.onmousedown = this._onUserActivity.bind(this); document.onmousemove = this._onUserActivity.bind(this); - document.onkeypress = this._onUserActivity.bind(this); + document.onkeydown = this._onUserActivity.bind(this); // can't use document.scroll here because that's only the document // itself being scrolled. Need to use addEventListener's useCapture. // also this needs to be the wheel event, not scroll, as scroll is @@ -50,7 +50,7 @@ class UserActivity { stop() { document.onmousedown = undefined; document.onmousemove = undefined; - document.onkeypress = undefined; + document.onkeydown = undefined; window.removeEventListener('wheel', this._onUserActivity.bind(this), { passive: true, capture: true }); } diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 66a872958c..84d85e7565 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -14,26 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; -var q = require("q"); -var MatrixClientPeg = require("./MatrixClientPeg"); -var Notifier = require("./Notifier"); +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. */ -module.exports = { +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() { - var cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.get(); return cli.getProfileInfo(cli.credentials.userId); }, @@ -44,7 +49,7 @@ module.exports = { loadThreePids: function() { if (MatrixClientPeg.get().isGuest()) { return q({ - threepids: [] + threepids: [], }); // guests can't poke 3pid endpoint } return MatrixClientPeg.get().getThreePids(); @@ -73,19 +78,19 @@ module.exports = { Notifier.setAudioEnabled(enable); }, - changePassword: function(old_password, new_password) { - var cli = MatrixClientPeg.get(); + changePassword: function(oldPassword, newPassword) { + const cli = MatrixClientPeg.get(); - var authDict = { + const authDict = { type: 'm.login.password', user: cli.credentials.userId, - password: old_password + password: oldPassword, }; - return cli.setPassword(authDict, new_password); + return cli.setPassword(authDict, newPassword); }, - /** + /* * Returns the email pusher (pusher of type 'email') for a given * email address. Email pushers all have the same app ID, so since * pushers are unique over (app ID, pushkey), there will be at most @@ -95,8 +100,8 @@ module.exports = { if (pushers === undefined) { return undefined; } - for (var i = 0; i < pushers.length; ++i) { - if (pushers[i].kind == 'email' && pushers[i].pushkey == address) { + for (let i = 0; i < pushers.length; ++i) { + if (pushers[i].kind === 'email' && pushers[i].pushkey === address) { return pushers[i]; } } @@ -110,7 +115,7 @@ module.exports = { addEmailPusher: function(address, data) { return MatrixClientPeg.get().setPusher({ kind: 'email', - app_id: "m.email", + app_id: 'm.email', pushkey: address, app_display_name: 'Email Notifications', device_display_name: address, @@ -121,46 +126,46 @@ module.exports = { }, getUrlPreviewsDisabled: function() { - var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); + const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls'); return (event && event.getContent().disable); }, setUrlPreviewsDisabled: function(disabled) { // FIXME: handle errors - return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { - disable: disabled + return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', { + disable: disabled, }); }, getSyncedSettings: function() { - var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings"); + const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings'); return event ? event.getContent() : {}; }, getSyncedSetting: function(type, defaultValue = null) { - var settings = this.getSyncedSettings(); - return settings.hasOwnProperty(type) ? settings[type] : null; + const settings = this.getSyncedSettings(); + return settings.hasOwnProperty(type) ? settings[type] : defaultValue; }, setSyncedSetting: function(type, value) { - var settings = this.getSyncedSettings(); + const settings = this.getSyncedSettings(); settings[type] = value; // FIXME: handle errors - return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); + return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings); }, getLocalSettings: function() { - var localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; + const localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; return JSON.parse(localSettingsString); }, getLocalSetting: function(type, defaultValue = null) { - var settings = this.getLocalSettings(); - return settings.hasOwnProperty(type) ? settings[type] : null; + const settings = this.getLocalSettings(); + return settings.hasOwnProperty(type) ? settings[type] : defaultValue; }, setLocalSetting: function(type, value) { - var settings = this.getLocalSettings(); + const settings = this.getLocalSettings(); settings[type] = value; // FIXME: handle errors localStorage.setItem('mx_local_settings', JSON.stringify(settings)); @@ -171,8 +176,8 @@ module.exports = { if (MatrixClientPeg.get().isGuest()) return false; if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) { - for (var i = 0; i < this.LABS_FEATURES.length; i++) { - var f = this.LABS_FEATURES[i]; + for (let i = 0; i < this.LABS_FEATURES.length; i++) { + const f = this.LABS_FEATURES[i]; if (f.id === feature) { return f.default; } @@ -183,5 +188,5 @@ module.exports = { setFeatureEnabled: function(feature: string, enabled: boolean) { localStorage.setItem(`mx_labs_feature_${feature}`, enabled); - } + }, }; diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index 4502b0ccd9..f3d89f0ff2 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -15,6 +15,7 @@ limitations under the License. */ var MatrixClientPeg = require("./MatrixClientPeg"); +import { _t } from './languageHandler'; module.exports = { usersTypingApartFromMe: function(room) { @@ -56,18 +57,18 @@ module.exports = { if (whoIsTyping.length == 0) { return ''; } else if (whoIsTyping.length == 1) { - return whoIsTyping[0].name + ' is typing'; + return _t('%(displayName)s is typing', {displayName: whoIsTyping[0].name}); } const names = whoIsTyping.map(function(m) { return m.name; }); - if (othersCount) { - const other = ' other' + (othersCount > 1 ? 's' : ''); - return names.slice(0, limit - 1).join(', ') + ' and ' + - othersCount + other + ' are typing'; + if (othersCount==1) { + return _t('%(names)s and one other are typing', {names: names.slice(0, limit - 1).join(', ')}); + } else if (othersCount>1) { + return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); } else { const lastPerson = names.pop(); - return names.join(', ') + ' and ' + lastPerson + ' are typing'; + return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson}); } } }; diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index ba706e0aa5..3a6ca4e6b7 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ var React = require("react"); +import { _t } from '../../../languageHandler'; var sdk = require('../../../index'); var MatrixClientPeg = require("../../../MatrixClientPeg"); @@ -78,33 +79,33 @@ module.exports = React.createClass({ _renderDeviceInfo: function() { var device = this.state.device; if (!device) { - return (unknown device); + return ({ _t('unknown device') }); } - var verificationStatus = (NOT verified); + var verificationStatus = ({ _t('NOT verified') }); if (device.isBlocked()) { - verificationStatus = (Blacklisted); + verificationStatus = ({ _t('Blacklisted') }); } else if (device.isVerified()) { - verificationStatus = "verified"; + verificationStatus = _t('verified'); } return ( - + - + - + - + @@ -119,32 +120,32 @@ module.exports = React.createClass({
Name{ _t('Name') } { device.getDisplayName() }
Device ID{ _t('Device ID') } { device.deviceId }
Verification{ _t('Verification') } { verificationStatus }
Ed25519 fingerprint{ _t('Ed25519 fingerprint') } {device.getFingerprint()}
- + - - + + - - + + - - + + { event.getContent().msgtype === 'm.bad.encrypted' ? ( - + ) : null } - - + +
User ID{ _t('User ID') } { event.getSender() }
Curve25519 identity key{ event.getSenderKey() || none }{ _t('Curve25519 identity key') }{ event.getSenderKey() || { _t('none') } }
Claimed Ed25519 fingerprint key{ event.getKeysClaimed().ed25519 || none }{ _t('Claimed Ed25519 fingerprint key') }{ event.getKeysClaimed().ed25519 || { _t('none') } }
Algorithm{ event.getWireContent().algorithm || unencrypted }{ _t('Algorithm') }{ event.getWireContent().algorithm || { _t('unencrypted') } }
Decryption error{ _t('Decryption error') } { event.getContent().body }
Session ID{ event.getWireContent().session_id || none }{ _t('Session ID') }{ event.getWireContent().session_id || { _t('none') } }
@@ -166,18 +167,18 @@ module.exports = React.createClass({ return (
- End-to-end encryption information + { _t('End-to-end encryption information') }
-

Event information

+

{ _t('Event information') }

{this._renderEventInfo()} -

Sender device information

+

{ _t('Sender device information') }

{this._renderDeviceInfo()}
{buttons}
diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 56b9d56cc9..045ea63c34 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -16,6 +16,7 @@ limitations under the License. import FileSaver from 'file-saver'; import React from 'react'; +import { _t } from '../../../languageHandler'; import * as Matrix from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; @@ -52,11 +53,11 @@ export default React.createClass({ const passphrase = this.refs.passphrase1.value; if (passphrase !== this.refs.passphrase2.value) { - this.setState({errStr: 'Passphrases must match'}); + this.setState({errStr: _t('Passphrases must match')}); return false; } if (!passphrase) { - this.setState({errStr: 'Passphrase must not be empty'}); + this.setState({errStr: _t('Passphrase must not be empty')}); return false; } @@ -109,24 +110,28 @@ export default React.createClass({ return (

- This process allows you to export the keys for messages - 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. + { _t( + 'This process allows you to export the keys for messages ' + + '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.', + ) }

- The exported file will allow anyone who can read it to decrypt - any encrypted messages that you can see, so you should be - 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. + { _t( + 'The exported file will allow anyone who can read it to decrypt ' + + 'any encrypted messages that you can see, so you should be ' + + '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.', + ) }

{this.state.errStr} @@ -135,7 +140,7 @@ export default React.createClass({
@@ -148,7 +153,7 @@ export default React.createClass({
@@ -161,11 +166,11 @@ export default React.createClass({
-
diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index ddd13813e2..91010d33b9 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -19,6 +19,7 @@ import React from 'react'; import * as Matrix from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; function readFileAsArrayBuffer(file) { return new Promise((resolve, reject) => { @@ -112,20 +113,23 @@ export default React.createClass({ return (

- 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. + { _t( + '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.', + ) }

- The export file will be protected with a passphrase. - You should enter the passphrase here, to decrypt the - file. + { _t( + 'The export file will be protected with a passphrase. ' + + 'You should enter the passphrase here, to decrypt the file.', + ) }

{this.state.errStr} @@ -134,7 +138,7 @@ export default React.createClass({
@@ -147,7 +151,7 @@ export default React.createClass({
@@ -160,11 +164,11 @@ export default React.createClass({
-
diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 5c90990295..cbdb839ce3 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -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'; diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 1bf1b1dc14..f8564a43a0 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -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'; diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 60171bc72f..205a3737dc 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -1,8 +1,27 @@ +/* +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 Fuse from 'fuse.js'; import {TextualCompletion} from './Components'; +// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file const COMMANDS = [ { command: '/me', @@ -43,10 +62,10 @@ const COMMANDS = [ command: '/ddg', args: '', description: 'Searches DuckDuckGo for results', - } + }, ]; -let COMMAND_RE = /(^\/\w*)/g; +const COMMAND_RE = /(^\/\w*)/g; let instance = null; @@ -60,15 +79,15 @@ export default class CommandProvider extends AutocompleteProvider { async getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - let {command, range} = this.getCurrentCommand(query, selection); + const {command, range} = this.getCurrentCommand(query, selection); if (command) { - completions = this.fuse.search(command[0]).map(result => { + completions = this.fuse.search(command[0]).map((result) => { return { completion: result.command + ' ', component: (), range, }; @@ -78,12 +97,11 @@ export default class CommandProvider extends AutocompleteProvider { } getName() { - return '*️⃣ Commands'; + return '*️⃣ ' + _t('Commands'); } static getInstance(): CommandProvider { - if (instance == null) - {instance = new CommandProvider();} + if (instance === null) instance = new CommandProvider(); return instance; } diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index 4595f7456d..0f0399cf7d 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -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 diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index bffd924976..9c996bb1cc 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -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 { diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index a2d77f02a1..810212315b 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -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 {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione'; import Fuse from 'fuse.js'; @@ -14,7 +32,7 @@ let instance = null; export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); - this.fuse = new Fuse(EMOJI_SHORTNAMES); + this.fuse = new Fuse(EMOJI_SHORTNAMES, {}); } async getCompletions(query: string, selection: SelectionRange) { @@ -39,7 +57,7 @@ export default class EmojiProvider extends AutocompleteProvider { } getName() { - return '😃 Emoji'; + return '😃 ' + _t('Emoji'); } static getInstance() { diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 8d1e555e56..be35c53e5d 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -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 MatrixClientPeg from '../MatrixClientPeg'; import Fuse from 'fuse.js'; @@ -50,7 +68,7 @@ export default class RoomProvider extends AutocompleteProvider { } getName() { - return '💬 Rooms'; + return '💬 ' + _t('Rooms'); } static getInstance() { diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 4d40fbdf94..809fec94be 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -1,6 +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'; @@ -51,7 +68,7 @@ export default class UserProvider extends AutocompleteProvider { } getName() { - return '👥 Users'; + return '👥 ' + _t('Users'); } setUserList(users) { diff --git a/src/component-index.js b/src/component-index.js deleted file mode 100644 index d6873c6dfd..0000000000 --- a/src/component-index.js +++ /dev/null @@ -1,253 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* - * THIS FILE IS AUTO-GENERATED - * You can edit it you like, but your changes will be overwritten, - * so you'd just be trying to swim upstream like a salmon. - * You are not a salmon. - * - * To update it, run: - * ./reskindex.js -h header - */ - -module.exports.components = {}; -import structures$ContextualMenu from './components/structures/ContextualMenu'; -structures$ContextualMenu && (module.exports.components['structures.ContextualMenu'] = structures$ContextualMenu); -import structures$CreateRoom from './components/structures/CreateRoom'; -structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom); -import structures$FilePanel from './components/structures/FilePanel'; -structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel); -import structures$InteractiveAuth from './components/structures/InteractiveAuth'; -structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth); -import structures$LoggedInView from './components/structures/LoggedInView'; -structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView); -import structures$MatrixChat from './components/structures/MatrixChat'; -structures$MatrixChat && (module.exports.components['structures.MatrixChat'] = structures$MatrixChat); -import structures$MessagePanel from './components/structures/MessagePanel'; -structures$MessagePanel && (module.exports.components['structures.MessagePanel'] = structures$MessagePanel); -import structures$NotificationPanel from './components/structures/NotificationPanel'; -structures$NotificationPanel && (module.exports.components['structures.NotificationPanel'] = structures$NotificationPanel); -import structures$RoomStatusBar from './components/structures/RoomStatusBar'; -structures$RoomStatusBar && (module.exports.components['structures.RoomStatusBar'] = structures$RoomStatusBar); -import structures$RoomView from './components/structures/RoomView'; -structures$RoomView && (module.exports.components['structures.RoomView'] = structures$RoomView); -import structures$ScrollPanel from './components/structures/ScrollPanel'; -structures$ScrollPanel && (module.exports.components['structures.ScrollPanel'] = structures$ScrollPanel); -import structures$TimelinePanel from './components/structures/TimelinePanel'; -structures$TimelinePanel && (module.exports.components['structures.TimelinePanel'] = structures$TimelinePanel); -import structures$UploadBar from './components/structures/UploadBar'; -structures$UploadBar && (module.exports.components['structures.UploadBar'] = structures$UploadBar); -import structures$UserSettings from './components/structures/UserSettings'; -structures$UserSettings && (module.exports.components['structures.UserSettings'] = structures$UserSettings); -import structures$login$ForgotPassword from './components/structures/login/ForgotPassword'; -structures$login$ForgotPassword && (module.exports.components['structures.login.ForgotPassword'] = structures$login$ForgotPassword); -import structures$login$Login from './components/structures/login/Login'; -structures$login$Login && (module.exports.components['structures.login.Login'] = structures$login$Login); -import structures$login$PostRegistration from './components/structures/login/PostRegistration'; -structures$login$PostRegistration && (module.exports.components['structures.login.PostRegistration'] = structures$login$PostRegistration); -import structures$login$Registration from './components/structures/login/Registration'; -structures$login$Registration && (module.exports.components['structures.login.Registration'] = structures$login$Registration); -import views$avatars$BaseAvatar from './components/views/avatars/BaseAvatar'; -views$avatars$BaseAvatar && (module.exports.components['views.avatars.BaseAvatar'] = views$avatars$BaseAvatar); -import views$avatars$MemberAvatar from './components/views/avatars/MemberAvatar'; -views$avatars$MemberAvatar && (module.exports.components['views.avatars.MemberAvatar'] = views$avatars$MemberAvatar); -import views$avatars$RoomAvatar from './components/views/avatars/RoomAvatar'; -views$avatars$RoomAvatar && (module.exports.components['views.avatars.RoomAvatar'] = views$avatars$RoomAvatar); -import views$create_room$CreateRoomButton from './components/views/create_room/CreateRoomButton'; -views$create_room$CreateRoomButton && (module.exports.components['views.create_room.CreateRoomButton'] = views$create_room$CreateRoomButton); -import views$create_room$Presets from './components/views/create_room/Presets'; -views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets); -import views$create_room$RoomAlias from './components/views/create_room/RoomAlias'; -views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias); -import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog'; -views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog); -import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/ChatCreateOrReuseDialog'; -views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog); -import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog'; -views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog); -import views$dialogs$ConfirmRedactDialog from './components/views/dialogs/ConfirmRedactDialog'; -views$dialogs$ConfirmRedactDialog && (module.exports.components['views.dialogs.ConfirmRedactDialog'] = views$dialogs$ConfirmRedactDialog); -import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog'; -views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog); -import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog'; -views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog); -import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog'; -views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog); -import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog'; -views$dialogs$InteractiveAuthDialog && (module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog); -import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog'; -views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog); -import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog'; -views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog); -import views$dialogs$SessionRestoreErrorDialog from './components/views/dialogs/SessionRestoreErrorDialog'; -views$dialogs$SessionRestoreErrorDialog && (module.exports.components['views.dialogs.SessionRestoreErrorDialog'] = views$dialogs$SessionRestoreErrorDialog); -import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog'; -views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog); -import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog'; -views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog); -import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog'; -views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog); -import views$elements$AccessibleButton from './components/views/elements/AccessibleButton'; -views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton); -import views$elements$AddressSelector from './components/views/elements/AddressSelector'; -views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector); -import views$elements$AddressTile from './components/views/elements/AddressTile'; -views$elements$AddressTile && (module.exports.components['views.elements.AddressTile'] = views$elements$AddressTile); -import views$elements$DeviceVerifyButtons from './components/views/elements/DeviceVerifyButtons'; -views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons); -import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox'; -views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox); -import views$elements$Dropdown from './components/views/elements/Dropdown'; -views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown); -import views$elements$EditableText from './components/views/elements/EditableText'; -views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText); -import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer'; -views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer); -import views$elements$EmojiText from './components/views/elements/EmojiText'; -views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText); -import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary'; -views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary); -import views$elements$PowerSelector from './components/views/elements/PowerSelector'; -views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector); -import views$elements$ProgressBar from './components/views/elements/ProgressBar'; -views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar); -import views$elements$TintableSvg from './components/views/elements/TintableSvg'; -views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg); -import views$elements$TruncatedList from './components/views/elements/TruncatedList'; -views$elements$TruncatedList && (module.exports.components['views.elements.TruncatedList'] = views$elements$TruncatedList); -import views$elements$UserSelector from './components/views/elements/UserSelector'; -views$elements$UserSelector && (module.exports.components['views.elements.UserSelector'] = views$elements$UserSelector); -import views$login$CaptchaForm from './components/views/login/CaptchaForm'; -views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm); -import views$login$CasLogin from './components/views/login/CasLogin'; -views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin); -import views$login$CountryDropdown from './components/views/login/CountryDropdown'; -views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown); -import views$login$CustomServerDialog from './components/views/login/CustomServerDialog'; -views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog); -import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents'; -views$login$InteractiveAuthEntryComponents && (module.exports.components['views.login.InteractiveAuthEntryComponents'] = views$login$InteractiveAuthEntryComponents); -import views$login$LoginFooter from './components/views/login/LoginFooter'; -views$login$LoginFooter && (module.exports.components['views.login.LoginFooter'] = views$login$LoginFooter); -import views$login$LoginHeader from './components/views/login/LoginHeader'; -views$login$LoginHeader && (module.exports.components['views.login.LoginHeader'] = views$login$LoginHeader); -import views$login$PasswordLogin from './components/views/login/PasswordLogin'; -views$login$PasswordLogin && (module.exports.components['views.login.PasswordLogin'] = views$login$PasswordLogin); -import views$login$RegistrationForm from './components/views/login/RegistrationForm'; -views$login$RegistrationForm && (module.exports.components['views.login.RegistrationForm'] = views$login$RegistrationForm); -import views$login$ServerConfig from './components/views/login/ServerConfig'; -views$login$ServerConfig && (module.exports.components['views.login.ServerConfig'] = views$login$ServerConfig); -import views$messages$MAudioBody from './components/views/messages/MAudioBody'; -views$messages$MAudioBody && (module.exports.components['views.messages.MAudioBody'] = views$messages$MAudioBody); -import views$messages$MFileBody from './components/views/messages/MFileBody'; -views$messages$MFileBody && (module.exports.components['views.messages.MFileBody'] = views$messages$MFileBody); -import views$messages$MImageBody from './components/views/messages/MImageBody'; -views$messages$MImageBody && (module.exports.components['views.messages.MImageBody'] = views$messages$MImageBody); -import views$messages$MVideoBody from './components/views/messages/MVideoBody'; -views$messages$MVideoBody && (module.exports.components['views.messages.MVideoBody'] = views$messages$MVideoBody); -import views$messages$MessageEvent from './components/views/messages/MessageEvent'; -views$messages$MessageEvent && (module.exports.components['views.messages.MessageEvent'] = views$messages$MessageEvent); -import views$messages$SenderProfile from './components/views/messages/SenderProfile'; -views$messages$SenderProfile && (module.exports.components['views.messages.SenderProfile'] = views$messages$SenderProfile); -import views$messages$TextualBody from './components/views/messages/TextualBody'; -views$messages$TextualBody && (module.exports.components['views.messages.TextualBody'] = views$messages$TextualBody); -import views$messages$TextualEvent from './components/views/messages/TextualEvent'; -views$messages$TextualEvent && (module.exports.components['views.messages.TextualEvent'] = views$messages$TextualEvent); -import views$messages$UnknownBody from './components/views/messages/UnknownBody'; -views$messages$UnknownBody && (module.exports.components['views.messages.UnknownBody'] = views$messages$UnknownBody); -import views$room_settings$AliasSettings from './components/views/room_settings/AliasSettings'; -views$room_settings$AliasSettings && (module.exports.components['views.room_settings.AliasSettings'] = views$room_settings$AliasSettings); -import views$room_settings$ColorSettings from './components/views/room_settings/ColorSettings'; -views$room_settings$ColorSettings && (module.exports.components['views.room_settings.ColorSettings'] = views$room_settings$ColorSettings); -import views$room_settings$UrlPreviewSettings from './components/views/room_settings/UrlPreviewSettings'; -views$room_settings$UrlPreviewSettings && (module.exports.components['views.room_settings.UrlPreviewSettings'] = views$room_settings$UrlPreviewSettings); -import views$rooms$Autocomplete from './components/views/rooms/Autocomplete'; -views$rooms$Autocomplete && (module.exports.components['views.rooms.Autocomplete'] = views$rooms$Autocomplete); -import views$rooms$AuxPanel from './components/views/rooms/AuxPanel'; -views$rooms$AuxPanel && (module.exports.components['views.rooms.AuxPanel'] = views$rooms$AuxPanel); -import views$rooms$EntityTile from './components/views/rooms/EntityTile'; -views$rooms$EntityTile && (module.exports.components['views.rooms.EntityTile'] = views$rooms$EntityTile); -import views$rooms$EventTile from './components/views/rooms/EventTile'; -views$rooms$EventTile && (module.exports.components['views.rooms.EventTile'] = views$rooms$EventTile); -import views$rooms$LinkPreviewWidget from './components/views/rooms/LinkPreviewWidget'; -views$rooms$LinkPreviewWidget && (module.exports.components['views.rooms.LinkPreviewWidget'] = views$rooms$LinkPreviewWidget); -import views$rooms$MemberDeviceInfo from './components/views/rooms/MemberDeviceInfo'; -views$rooms$MemberDeviceInfo && (module.exports.components['views.rooms.MemberDeviceInfo'] = views$rooms$MemberDeviceInfo); -import views$rooms$MemberInfo from './components/views/rooms/MemberInfo'; -views$rooms$MemberInfo && (module.exports.components['views.rooms.MemberInfo'] = views$rooms$MemberInfo); -import views$rooms$MemberList from './components/views/rooms/MemberList'; -views$rooms$MemberList && (module.exports.components['views.rooms.MemberList'] = views$rooms$MemberList); -import views$rooms$MemberTile from './components/views/rooms/MemberTile'; -views$rooms$MemberTile && (module.exports.components['views.rooms.MemberTile'] = views$rooms$MemberTile); -import views$rooms$MessageComposer from './components/views/rooms/MessageComposer'; -views$rooms$MessageComposer && (module.exports.components['views.rooms.MessageComposer'] = views$rooms$MessageComposer); -import views$rooms$MessageComposerInput from './components/views/rooms/MessageComposerInput'; -views$rooms$MessageComposerInput && (module.exports.components['views.rooms.MessageComposerInput'] = views$rooms$MessageComposerInput); -import views$rooms$MessageComposerInputOld from './components/views/rooms/MessageComposerInputOld'; -views$rooms$MessageComposerInputOld && (module.exports.components['views.rooms.MessageComposerInputOld'] = views$rooms$MessageComposerInputOld); -import views$rooms$PresenceLabel from './components/views/rooms/PresenceLabel'; -views$rooms$PresenceLabel && (module.exports.components['views.rooms.PresenceLabel'] = views$rooms$PresenceLabel); -import views$rooms$ReadReceiptMarker from './components/views/rooms/ReadReceiptMarker'; -views$rooms$ReadReceiptMarker && (module.exports.components['views.rooms.ReadReceiptMarker'] = views$rooms$ReadReceiptMarker); -import views$rooms$RoomHeader from './components/views/rooms/RoomHeader'; -views$rooms$RoomHeader && (module.exports.components['views.rooms.RoomHeader'] = views$rooms$RoomHeader); -import views$rooms$RoomList from './components/views/rooms/RoomList'; -views$rooms$RoomList && (module.exports.components['views.rooms.RoomList'] = views$rooms$RoomList); -import views$rooms$RoomNameEditor from './components/views/rooms/RoomNameEditor'; -views$rooms$RoomNameEditor && (module.exports.components['views.rooms.RoomNameEditor'] = views$rooms$RoomNameEditor); -import views$rooms$RoomPreviewBar from './components/views/rooms/RoomPreviewBar'; -views$rooms$RoomPreviewBar && (module.exports.components['views.rooms.RoomPreviewBar'] = views$rooms$RoomPreviewBar); -import views$rooms$RoomSettings from './components/views/rooms/RoomSettings'; -views$rooms$RoomSettings && (module.exports.components['views.rooms.RoomSettings'] = views$rooms$RoomSettings); -import views$rooms$RoomTile from './components/views/rooms/RoomTile'; -views$rooms$RoomTile && (module.exports.components['views.rooms.RoomTile'] = views$rooms$RoomTile); -import views$rooms$RoomTopicEditor from './components/views/rooms/RoomTopicEditor'; -views$rooms$RoomTopicEditor && (module.exports.components['views.rooms.RoomTopicEditor'] = views$rooms$RoomTopicEditor); -import views$rooms$SearchResultTile from './components/views/rooms/SearchResultTile'; -views$rooms$SearchResultTile && (module.exports.components['views.rooms.SearchResultTile'] = views$rooms$SearchResultTile); -import views$rooms$SearchableEntityList from './components/views/rooms/SearchableEntityList'; -views$rooms$SearchableEntityList && (module.exports.components['views.rooms.SearchableEntityList'] = views$rooms$SearchableEntityList); -import views$rooms$SimpleRoomHeader from './components/views/rooms/SimpleRoomHeader'; -views$rooms$SimpleRoomHeader && (module.exports.components['views.rooms.SimpleRoomHeader'] = views$rooms$SimpleRoomHeader); -import views$rooms$TabCompleteBar from './components/views/rooms/TabCompleteBar'; -views$rooms$TabCompleteBar && (module.exports.components['views.rooms.TabCompleteBar'] = views$rooms$TabCompleteBar); -import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnreadMessagesBar'; -views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar); -import views$rooms$UserTile from './components/views/rooms/UserTile'; -views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile); -import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber'; -views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber); -import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar'; -views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar); -import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName'; -views$settings$ChangeDisplayName && (module.exports.components['views.settings.ChangeDisplayName'] = views$settings$ChangeDisplayName); -import views$settings$ChangePassword from './components/views/settings/ChangePassword'; -views$settings$ChangePassword && (module.exports.components['views.settings.ChangePassword'] = views$settings$ChangePassword); -import views$settings$DevicesPanel from './components/views/settings/DevicesPanel'; -views$settings$DevicesPanel && (module.exports.components['views.settings.DevicesPanel'] = views$settings$DevicesPanel); -import views$settings$DevicesPanelEntry from './components/views/settings/DevicesPanelEntry'; -views$settings$DevicesPanelEntry && (module.exports.components['views.settings.DevicesPanelEntry'] = views$settings$DevicesPanelEntry); -import views$settings$EnableNotificationsButton from './components/views/settings/EnableNotificationsButton'; -views$settings$EnableNotificationsButton && (module.exports.components['views.settings.EnableNotificationsButton'] = views$settings$EnableNotificationsButton); -import views$voip$CallView from './components/views/voip/CallView'; -views$voip$CallView && (module.exports.components['views.voip.CallView'] = views$voip$CallView); -import views$voip$IncomingCallBox from './components/views/voip/IncomingCallBox'; -views$voip$IncomingCallBox && (module.exports.components['views.voip.IncomingCallBox'] = views$voip$IncomingCallBox); -import views$voip$VideoFeed from './components/views/voip/VideoFeed'; -views$voip$VideoFeed && (module.exports.components['views.voip.VideoFeed'] = views$voip$VideoFeed); -import views$voip$VideoView from './components/views/voip/VideoView'; -views$voip$VideoView && (module.exports.components['views.voip.VideoView'] = views$voip$VideoView); diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index 24ebfea07f..7ecc315ba7 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -16,15 +16,15 @@ limitations under the License. 'use strict'; -var React = require("react"); -var MatrixClientPeg = require("../../MatrixClientPeg"); -var PresetValues = { +import React from 'react'; +import { _t } from '../../languageHandler'; +import sdk from '../../index'; +import MatrixClientPeg from '../../MatrixClientPeg'; +const PresetValues = { PrivateChat: "private_chat", PublicChat: "public_chat", Custom: "custom", }; -var q = require('q'); -var sdk = require('../../index'); module.exports = React.createClass({ displayName: 'CreateRoom', @@ -231,7 +231,7 @@ module.exports = React.createClass({ if (curr_phase == this.phases.ERROR) { error_box = (
- An error occured: {this.state.error_string} + {_t('An error occurred: %(error_string)s', {error_string: this.state.error_string})}
); } @@ -246,29 +246,29 @@ module.exports = React.createClass({ return (
- +
-
- + , ); - var error; - var addressSelector; + let error; + let addressSelector; if (this.state.error) { - error =
You have entered an invalid contact. Try using their Matrix ID or email address.
; + error =
{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}
; + } else if (this.state.searchError) { + error =
{this.state.searchError}
; + } else if ( + this.state.query.length > 0 && + this.state.queryList.length === 0 && + !this.state.busy + ) { + error =
{_t("No results")}
; } else { - const addressSelectorHeader =
- Searching known users -
; addressSelector = ( {this.addressSelector = ref;}} addressList={ this.state.queryList } onSelected={ this.onSelected } truncateAt={ TRUNCATE_QUERY_LIST } - header={ addressSelectorHeader } /> ); } diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js index fc9e55f666..7922b7b289 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.js +++ b/src/components/views/dialogs/ConfirmRedactDialog.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../index'; import classnames from 'classnames'; +import { _t } from '../../../languageHandler'; /* * A dialog for confirming a redaction. @@ -42,7 +43,7 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const title = "Confirm Redaction"; + const title = _t("Confirm Removal"); const confirmButtonClass = classnames({ 'mx_Dialog_primary': true, @@ -55,16 +56,16 @@ export default React.createClass({ title={title} >
- Are you sure you wish to redact (delete) this event? - Note that if you redact a room name or topic change, it could undo the change. + {_t("Are you sure you wish to remove (delete) this event? " + + "Note that if you delete a room name or topic change, it could undo the change.")}
diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 6cfaac65d4..b10df3ccef 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; import classnames from 'classnames'; /* @@ -69,7 +70,7 @@ export default React.createClass({ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); - const title = this.props.action + " this person?"; + const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action}); const confirmButtonClass = classnames({ 'mx_Dialog_primary': true, 'danger': this.props.danger, @@ -82,7 +83,7 @@ export default React.createClass({
@@ -111,7 +112,7 @@ export default React.createClass({
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index b4879982bf..e3b7cca078 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -20,6 +20,7 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import * as Lifecycle from '../../../Lifecycle'; import Velocity from 'velocity-vector'; +import { _t } from '../../../languageHandler'; export default class DeactivateAccountDialog extends React.Component { constructor(props, context) { @@ -56,10 +57,10 @@ export default class DeactivateAccountDialog extends React.Component { Lifecycle.onLoggedOut(); this.props.onFinished(false); }, (err) => { - let errStr = 'Unknown error'; + let errStr = _t('Unknown error'); // https://matrix.org/jira/browse/SYN-744 if (err.httpStatus == 401 || err.httpStatus == 403) { - errStr = 'Incorrect password'; + errStr = _t('Incorrect password'); Velocity(this._passwordField, "callout.shake", 300); } this.setState({ @@ -85,29 +86,29 @@ export default class DeactivateAccountDialog extends React.Component { passwordBoxClass = 'error'; } - const okLabel = this.state.busy ? : 'Deactivate Account'; + const okLabel = this.state.busy ? : _t('Deactivate Account'); const okEnabled = this.state.confirmButtonEnabled && !this.state.busy; let cancelButton = null; if (!this.state.busy) { cancelButton = ; } return (
- Deactivate Account + {_t("Deactivate Account")}
-

This will make your account permanently unusable. You will not be able to re-register the same user ID.

+

{_t("This will make your account permanently unusable. You will not be able to re-register the same user ID.")}

-

This action is irreversible.

+

{_t("This action is irreversible.")}

-

To continue, please enter your password.

+

{_t("To continue, please enter your password.")}

-

Password:

+

{_t("Password")}:

+

+ {_t("To verify that this device can be trusted, please contact its " + + "owner using some other means (e.g. in person or a phone call) " + + "and ask them whether the key they see in their User Settings " + + "for this device matches the key below:")} +

+
+
    +
  • { props.device.getDisplayName() }
  • +
  • { props.device.deviceId}
  • +
  • { key }
  • +
+
+

+ {_t("If it matches, press the verify button below. " + + "If it doesn't, then someone else is intercepting this device " + + "and you probably want to press the blacklist button instead.")} +

+

+ {_t("In future this verification process will be more sophisticated.")} +

+
+ ); + + function onFinished(confirm) { + if (confirm) { + MatrixClientPeg.get().setDeviceVerified( + props.userId, props.device.deviceId, true, + ); + } + props.onFinished(confirm); + } + + return ( + + ); +} + +DeviceVerifyDialog.propTypes = { + userId: React.PropTypes.string.isRequired, + device: React.PropTypes.object.isRequired, + onFinished: React.PropTypes.func.isRequired, +}; diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index 937595dfa8..bf48d1757b 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -27,6 +27,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; export default React.createClass({ displayName: 'ErrorDialog', @@ -43,24 +44,30 @@ export default React.createClass({ getDefaultProps: function() { return { - title: "Error", - description: "An error has occurred.", - button: "OK", focus: true, + title: null, + description: null, + button: null, }; }, + componentDidMount: function() { + if (this.props.focus) { + this.refs.button.focus(); + } + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( + title={this.props.title || _t('Error')}>
- {this.props.description} + {this.props.description || _t('An error has occurred.')}
-
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index 145b4b6453..363ce89b57 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -15,11 +15,10 @@ 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'; +import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; @@ -46,12 +45,6 @@ export default React.createClass({ title: React.PropTypes.string, }, - getDefaultProps: function() { - return { - title: "Authentication", - }; - }, - getInitialState: function() { return { authError: null, @@ -85,7 +78,7 @@ export default React.createClass({ - Dismiss + {_t("Dismiss")}
); @@ -105,7 +98,7 @@ export default React.createClass({ return ( {content} diff --git a/src/components/views/dialogs/NeedToRegisterDialog.js b/src/components/views/dialogs/NeedToRegisterDialog.js index f4df5913d5..8b5980164d 100644 --- a/src/components/views/dialogs/NeedToRegisterDialog.js +++ b/src/components/views/dialogs/NeedToRegisterDialog.js @@ -26,6 +26,7 @@ limitations under the License. import React from 'react'; import dis from '../../../dispatcher'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'NeedToRegisterDialog', @@ -38,13 +39,6 @@ module.exports = React.createClass({ onFinished: React.PropTypes.func.isRequired, }, - getDefaultProps: function() { - return { - title: "Registration required", - description: "A registered account is required for this action", - }; - }, - onRegisterClicked: function() { dis.dispatch({ action: "start_upgrade_registration", @@ -59,17 +53,17 @@ module.exports = React.createClass({ return (
- {this.props.description} + {this.props.description || _t('A registered account is required for this action')}
diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 6012541b94..ec9b95d7f7 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; export default React.createClass({ displayName: 'QuestionDialog', @@ -33,7 +34,6 @@ export default React.createClass({ title: "", description: "", extraButtons: null, - button: "OK", focus: true, hasCancelButton: true, }; @@ -51,7 +51,7 @@ export default React.createClass({ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const cancelButton = this.props.hasCancelButton ? ( ) : null; return ( @@ -64,7 +64,7 @@ export default React.createClass({
{this.props.extraButtons} {cancelButton} diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index 358bbf1fec..a3eb7c6962 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -18,6 +18,7 @@ import React from 'react'; import sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; +import { _t, _tJsx } from '../../../languageHandler'; export default React.createClass({ @@ -43,29 +44,32 @@ export default React.createClass({ if (SdkConfig.get().bug_report_endpoint_url) { bugreport = ( -

Otherwise, - click here to send a bug report. +

+ {_tJsx( + "Otherwise, click here to send a bug report.", + /(.*?)<\/a>/, (sub) => {sub}, + )}

); } return ( + title={_t('Unable to restore session')}>
-

We encountered an error trying to restore your previous session. If - you continue, you will need to log in again, and encrypted chat - history will be unreadable.

+

{_t("We encountered an error trying to restore your previous session. If " + + "you continue, you will need to log in again, and encrypted chat " + + "history will be unreadable.")}

-

If you have previously used a more recent version of Riot, your session - may be incompatible with this version. Close this window and return - to the more recent version.

+

{_t("If you have previously used a more recent version of Riot, your session " + + "may be incompatible with this version. Close this window and return " + + "to the more recent version.")}

{bugreport}
diff --git a/src/components/views/dialogs/SetDisplayNameDialog.js b/src/components/views/dialogs/SetDisplayNameDialog.js deleted file mode 100644 index 1047e05c26..0000000000 --- a/src/components/views/dialogs/SetDisplayNameDialog.js +++ /dev/null @@ -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 ( - -
- Your display name is how you'll appear to others when you speak in rooms.
- What would you like it to be? -
-
-
- -
-
- -
-
-
- ); - }, -}); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js new file mode 100644 index 0000000000..d428223ad6 --- /dev/null +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -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 = ; + } + const inputClasses = classnames({ + "mx_SetMxIdDialog_input": true, + "error": Boolean(this.state.usernameError), + }); + + let usernameIndicator = null; + let usernameBusyIndicator = null; + if (this.state.usernameBusy) { + usernameBusyIndicator = ; + } else { + const usernameAvailable = this.state.username && + this.state.usernameCheckSupport && !this.state.usernameError; + const usernameIndicatorClasses = classnames({ + "error": Boolean(this.state.usernameError), + "success": usernameAvailable, + }); + usernameIndicator =
+ { usernameAvailable ? _t('Username available') : this.state.usernameError } +
; + } + + let authErrorIndicator = null; + if (this.state.authError) { + authErrorIndicator =
+ { this.state.authError } +
; + } + const canContinue = this.state.username && + !this.state.usernameError && + !this.state.usernameBusy; + + return ( + +
+
+ + { usernameBusyIndicator } +
+ { usernameIndicator } +

+ { _tJsx( + 'This will be your account name on the ' + + 'homeserver, or you can pick a different server.', + [ + /<\/span>/, + /(.*?)<\/a>/, + ], + [ + (sub) => {this.props.homeserverUrl}, + (sub) => {sub}, + ], + )} +

+

+ { _tJsx( + 'If you already have a Matrix account you can log in instead.', + /(.*?)<\/a>/, + [(sub) => {sub}], + )} +

+ { auth } + { authErrorIndicator } +
+
+ +
+
+ ); + }, +}); diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js index 6e40efffd8..673be42030 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; export default React.createClass({ displayName: 'TextInputDialog', @@ -36,7 +37,6 @@ export default React.createClass({ title: "", value: "", description: "", - button: "OK", focus: true, }; }, @@ -73,7 +73,7 @@ export default React.createClass({
); } else { blacklistButton = ( ); } @@ -130,14 +99,14 @@ export default React.createClass({ verifyButton = ( ); } else { verifyButton = ( ); } diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.js index 467878caad..eaa6ee34ba 100644 --- a/src/components/views/elements/DirectorySearchBox.js +++ b/src/components/views/elements/DirectorySearchBox.js @@ -93,7 +93,7 @@ export default class DirectorySearchBox extends React.Component { className="mx_DirectorySearchBox_input" ref={this._collectInput} onChange={this._onChange} onKeyUp={this._onKeyUp} - placeholder={this.props.placeholder} + placeholder={this.props.placeholder} autoFocus /> {join_button} diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 3b34d3cac1..c049c38a68 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import classnames from 'classnames'; import AccessibleButton from './AccessibleButton'; +import { _t } from '../../../languageHandler'; class MenuOption extends React.Component { constructor(props) { @@ -114,8 +115,11 @@ export default class Dropdown extends React.Component { } componentWillReceiveProps(nextProps) { + if (!nextProps.children || nextProps.children.length === 0) { + return; + } this._reindexChildren(nextProps.children); - const firstChild = React.Children.toArray(nextProps.children)[0]; + const firstChild = nextProps.children[0]; this.setState({ highlightedOption: firstChild ? firstChild.key : null, }); @@ -149,10 +153,12 @@ export default class Dropdown extends React.Component { } _onInputClick(ev) { - this.setState({ - expanded: !this.state.expanded, - }); - ev.preventDefault(); + if (!this.state.expanded) { + this.setState({ + expanded: true, + }); + ev.preventDefault(); + } } _onMenuOptionClick(dropdownKey) { @@ -248,13 +254,10 @@ export default class Dropdown extends React.Component { ); }); - - if (!this.state.searchQuery) { - options.push( -
- Type to search... -
- ); + if (options.length === 0) { + return [
+ {_t("No results")} +
]; } return options; } @@ -267,16 +270,20 @@ export default class Dropdown extends React.Component { let menu; if (this.state.expanded) { - currentValue = ; + if (this.props.searchEnabled) { + currentValue = ; + } menu =
{this._getMenuOptions()}
; - } else { + } + + if (!currentValue) { const selectedChild = this.props.getShortOption ? this.props.getShortOption(this.props.value) : this.childrenByKey[this.props.value]; @@ -313,6 +320,7 @@ Dropdown.propTypes = { onOptionChange: React.PropTypes.func.isRequired, // Called when the value of the search field changes onSearchChange: React.PropTypes.func, + searchEnabled: React.PropTypes.bool, // Function that, given the key of an option, returns // a node representing that option to be displayed in the // box itself as the currently-selected option (ie. as diff --git a/src/components/views/elements/HomeButton.js b/src/components/views/elements/HomeButton.js new file mode 100644 index 0000000000..4877f5dd43 --- /dev/null +++ b/src/components/views/elements/HomeButton.js @@ -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 ( + + ); +}; + +HomeButton.propTypes = { + size: PropTypes.string, + tooltip: PropTypes.bool, +}; + +export default HomeButton; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js new file mode 100644 index 0000000000..c5ed1f8942 --- /dev/null +++ b/src/components/views/elements/LanguageDropdown.js @@ -0,0 +1,120 @@ +/* +Copyright 2017 Marcel Radzio (MTRNord) +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 UserSettingsStore from '../../../UserSettingsStore'; +import * as languageHandler from '../../../languageHandler'; + +function languageMatchesSearchQuery(query, language) { + if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; + if (language.value.toUpperCase() == query.toUpperCase()) return true; + return false; +} + +export default class LanguageDropdown extends React.Component { + constructor(props) { + super(props); + this._onSearchChange = this._onSearchChange.bind(this); + + this.state = { + searchQuery: '', + langs: null, + } + } + + componentWillMount() { + languageHandler.getAllLanguagesFromJson().then((langs) => { + langs.sort(function(a, b){ + if(a.label < b.label) return -1; + if(a.label > b.label) return 1; + return 0; + }); + this.setState({langs}); + }).catch(() => { + this.setState({langs: ['en']}); + }).done(); + + if (!this.props.value) { + // If no value is given, we start with the first + // country selected, but our parent component + // doesn't know this, therefore we do this. + const _localSettings = UserSettingsStore.getLocalSettings(); + if (_localSettings.hasOwnProperty('language')) { + this.props.onOptionChange(_localSettings.language); + }else { + const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); + this.props.onOptionChange(language); + } + } + } + + _onSearchChange(search) { + this.setState({ + searchQuery: search, + }); + } + + render() { + if (this.state.langs === null) { + const Spinner = sdk.getComponent('elements.Spinner'); + return ; + } + + const Dropdown = sdk.getComponent('elements.Dropdown'); + + let displayedLanguages; + if (this.state.searchQuery) { + displayedLanguages = this.state.langs.filter((lang) => { + return languageMatchesSearchQuery(this.state.searchQuery, lang); + }); + } else { + displayedLanguages = this.state.langs; + } + + const options = displayedLanguages.map((language) => { + return
+ {language.label} +
; + }); + + // default value here too, otherwise we need to handle null / undefined + // values between mounting and the initial value propgating + let value = null; + const _localSettings = UserSettingsStore.getLocalSettings(); + if (_localSettings.hasOwnProperty('language')) { + value = this.props.value || _localSettings.language; + } else { + const language = navigator.language || navigator.userLanguage; + value = this.props.value || language; + } + + return + {options} + + } +} + +LanguageDropdown.propTypes = { + className: React.PropTypes.string, + onOptionChange: React.PropTypes.func.isRequired, + value: React.PropTypes.string, +}; diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index d7f876c16e..f2f6da83e5 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -14,7 +14,9 @@ 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'; module.exports = React.createClass({ displayName: 'MemberEventListSummary', @@ -110,9 +112,13 @@ module.exports = React.createClass({ return null; } + const EmojiText = sdk.getComponent('elements.EmojiText'); + return ( - {summaries.join(", ")} + + {summaries.join(", ")} + ); }, @@ -203,28 +209,146 @@ module.exports = React.createClass({ * @param {boolean} plural whether there were multiple users undergoing the same * transition. * @param {number} repeats the number of times the transition was repeated in a row. - * @returns {string} the written English equivalent of the transition. + * @returns {string} the written Human Readable equivalent of the transition. */ _getDescriptionForTransition(t, plural, repeats) { - const beConjugated = plural ? "were" : "was"; - const invitation = "their invitation" + (plural || (repeats > 1) ? "s" : ""); - + // The empty interpolations 'severalUsers' and 'oneUser' + // are there only to show translators to non-English languages + // that the verb is conjugated to plural or singular Subject. let res = null; - const map = { - "joined": "joined", - "left": "left", - "joined_and_left": "joined and left", - "left_and_joined": "left and rejoined", - "invite_reject": "rejected " + invitation, - "invite_withdrawal": "had " + invitation + " withdrawn", - "invited": beConjugated + " invited", - "banned": beConjugated + " banned", - "unbanned": beConjugated + " unbanned", - "kicked": beConjugated + " kicked", - }; + switch(t) { + case "joined": + if (repeats > 1) { + res = (plural) + ? _t("%(severalUsers)sjoined %(repeats)s times", { severalUsers: "", repeats: repeats }) + : _t("%(oneUser)sjoined %(repeats)s times", { oneUser: "", repeats: repeats }); + } else { + res = (plural) + ? _t("%(severalUsers)sjoined", { severalUsers: "" }) + : _t("%(oneUser)sjoined", { oneUser: "" }); + } - if (Object.keys(map).includes(t)) { - res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" ); + break; + case "left": + if (repeats > 1) { + res = (plural) + ? _t("%(severalUsers)sleft %(repeats)s times", { severalUsers: "", repeats: repeats }) + : _t("%(oneUser)sleft %(repeats)s times", { oneUser: "", repeats: repeats }); + } else { + res = (plural) + ? _t("%(severalUsers)sleft", { severalUsers: "" }) + : _t("%(oneUser)sleft", { oneUser: "" }); + } break; + case "joined_and_left": + if (repeats > 1) { + res = (plural) + ? _t("%(severalUsers)sjoined and left %(repeats)s times", { severalUsers: "", repeats: repeats }) + : _t("%(oneUser)sjoined and left %(repeats)s times", { oneUser: "", repeats: repeats }); + } else { + res = (plural) + ? _t("%(severalUsers)sjoined and left", { severalUsers: "" }) + : _t("%(oneUser)sjoined and left", { oneUser: "" }); + } + break; + case "left_and_joined": + if (repeats > 1) { + res = (plural) + ? _t("%(severalUsers)sleft and rejoined %(repeats)s times", { severalUsers: "", repeats: repeats }) + : _t("%(oneUser)sleft and rejoined %(repeats)s times", { oneUser: "", repeats: repeats }); + } else { + res = (plural) + ? _t("%(severalUsers)sleft and rejoined", { severalUsers: "" }) + : _t("%(oneUser)sleft and rejoined", { oneUser: "" }); + } break; + break; + case "invite_reject": + if (repeats > 1) { + res = (plural) + ? _t("%(severalUsers)srejected their invitations %(repeats)s times", { severalUsers: "", repeats: repeats }) + : _t("%(oneUser)srejected their invitation %(repeats)s times", { oneUser: "", repeats: repeats }); + } else { + res = (plural) + ? _t("%(severalUsers)srejected their invitations", { severalUsers: "" }) + : _t("%(oneUser)srejected their invitation", { oneUser: "" }); + } + break; + case "invite_withdrawal": + if (repeats > 1) { + res = (plural) + ? _t("%(severalUsers)shad their invitations withdrawn %(repeats)s times", { severalUsers: "", repeats: repeats }) + : _t("%(oneUser)shad their invitation withdrawn %(repeats)s times", { oneUser: "", repeats: repeats }); + } else { + res = (plural) + ? _t("%(severalUsers)shad their invitations withdrawn", { severalUsers: "" }) + : _t("%(oneUser)shad their invitation withdrawn", { oneUser: "" }); + } + break; + case "invited": + if (repeats > 1) { + res = (plural) + ? _t("were invited %(repeats)s times", { repeats: repeats }) + : _t("was invited %(repeats)s times", { repeats: repeats }); + } else { + res = (plural) + ? _t("were invited") + : _t("was invited"); + } + break; + case "banned": + if (repeats > 1) { + res = (plural) + ? _t("were banned %(repeats)s times", { repeats: repeats }) + : _t("was banned %(repeats)s times", { repeats: repeats }); + } else { + res = (plural) + ? _t("were banned") + : _t("was banned"); + } + break; + case "unbanned": + if (repeats > 1) { + res = (plural) + ? _t("were unbanned %(repeats)s times", { repeats: repeats }) + : _t("was unbanned %(repeats)s times", { repeats: repeats }); + } else { + res = (plural) + ? _t("were unbanned") + : _t("was unbanned"); + } + break; + case "kicked": + if (repeats > 1) { + res = (plural) + ? _t("were kicked %(repeats)s times", { repeats: repeats }) + : _t("was kicked %(repeats)s times", { repeats: repeats }); + } else { + res = (plural) + ? _t("were kicked") + : _t("was kicked"); + } + break; + case "changed_name": + if (repeats > 1) { + res = (plural) + ? _t("%(severalUsers)schanged their name %(repeats)s times", { severalUsers: "", repeats: repeats }) + : _t("%(oneUser)schanged their name %(repeats)s times", { oneUser: "", repeats: repeats }); + } else { + res = (plural) + ? _t("%(severalUsers)schanged their name", { severalUsers: "" }) + : _t("%(oneUser)schanged their name", { oneUser: "" }); + } + break; + case "changed_avatar": + if (repeats > 1) { + res = (plural) + ? _t("%(severalUsers)schanged their avatar %(repeats)s times", { severalUsers: "", repeats: repeats }) + : _t("%(oneUser)schanged their avatar %(repeats)s times", { oneUser: "", repeats: repeats }); + } else { + res = (plural) + ? _t("%(severalUsers)schanged their avatar", { severalUsers: "" }) + : _t("%(oneUser)schanged their avatar", { oneUser: "" }); + } + break; } return res; @@ -252,11 +376,12 @@ module.exports = React.createClass({ return items[0]; } else if (remaining) { items = items.slice(0, itemLimit); - const other = " other" + (remaining > 1 ? "s" : ""); - return items.join(', ') + ' and ' + remaining + other; + return (remaining > 1) + ? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } ) + : _t("%(items)s and one other", { items: items.join(', ') }); } else { const lastItem = items.pop(); - return items.join(', ') + ' and ' + lastItem; + return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); } }, @@ -267,7 +392,7 @@ module.exports = React.createClass({ ); }); return ( - + {avatars} ); @@ -289,7 +414,24 @@ module.exports = React.createClass({ switch (e.mxEvent.getContent().membership) { case 'invite': return 'invited'; case 'ban': return 'banned'; - case 'join': return 'joined'; + case 'join': + if (e.mxEvent.getPrevContent().membership === 'join') { + if (e.mxEvent.getContent().displayname !== + e.mxEvent.getPrevContent().displayname) + { + return 'changed_name'; + } + else if (e.mxEvent.getContent().avatar_url !== + e.mxEvent.getPrevContent().avatar_url) + { + return 'changed_avatar'; + } + // console.log("MELS ignoring duplicate membership join event"); + return null; + } + else { + return 'joined'; + } case 'leave': if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) { switch (e.mxEvent.getPrevContent().membership) { @@ -350,6 +492,7 @@ module.exports = React.createClass({ render: function() { const eventsToRender = this.props.events; + const eventIds = eventsToRender.map(e => e.getId()).join(','); const fewEvents = eventsToRender.length < this.props.threshold; const expanded = this.state.expanded || fewEvents; @@ -360,7 +503,7 @@ module.exports = React.createClass({ if (fewEvents) { return ( -
+
{expandedEvents}
); @@ -418,7 +561,7 @@ module.exports = React.createClass({ ); return ( -
+
{toggleButton} {summaryContainer} {expanded ?
 
: null} diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index c7bfd4eec1..efeb81fe2d 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -16,18 +16,12 @@ limitations under the License. 'use strict'; -var React = require('react'); - -var roles = { - 0: 'User', - 50: 'Moderator', - 100: 'Admin', -}; +import React from 'react'; +import * as Roles from '../../../Roles'; +import { _t } from '../../../languageHandler'; +var LEVEL_ROLE_MAP = {}; var reverseRoles = {}; -Object.keys(roles).forEach(function(key) { - reverseRoles[roles[key]] = key; -}); module.exports = React.createClass({ displayName: 'PowerSelector', @@ -49,9 +43,16 @@ module.exports = React.createClass({ getInitialState: function() { return { - custom: (roles[this.props.value] === undefined), + custom: (LEVEL_ROLE_MAP[this.props.value] === undefined), }; }, + + componentWillMount: function() { + LEVEL_ROLE_MAP = Roles.levelRoleMap(); + Object.keys(LEVEL_ROLE_MAP).forEach(function(key) { + reverseRoles[LEVEL_ROLE_MAP[key]] = key; + }); + }, onSelectChange: function(event) { this.setState({ custom: event.target.value === "Custom" }); @@ -99,22 +100,34 @@ module.exports = React.createClass({ selectValue = "Custom"; } else { - selectValue = roles[this.props.value] || "Custom"; + selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom"; } var select; if (this.props.disabled) { select = { selectValue }; } else { + // Each level must have a definition in LEVEL_ROLE_MAP + const levels = [0, 50, 100]; + let options = levels.map((level) => { + return { + value: LEVEL_ROLE_MAP[level], + // Give a userDefault (users_default in the power event) of 0 but + // because level !== undefined, this should never be used. + text: Roles.textualPowerLevel(level, 0), + } + }); + options.push({ value: "Custom", text: _t("Custom level") }); + options = options.map((op) => { + return ; + }); + select = ; } diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js new file mode 100644 index 0000000000..d964d9e5bc --- /dev/null +++ b/src/components/views/elements/RoomDirectoryButton.js @@ -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 ( + + ); +}; + +RoomDirectoryButton.propTypes = { + size: PropTypes.string, + tooltip: PropTypes.bool, +}; + +export default RoomDirectoryButton; diff --git a/src/components/views/elements/SettingsButton.js b/src/components/views/elements/SettingsButton.js new file mode 100644 index 0000000000..ad09971689 --- /dev/null +++ b/src/components/views/elements/SettingsButton.js @@ -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 ( + + ); +}; + +SettingsButton.propTypes = { + size: PropTypes.string, + tooltip: PropTypes.bool, +}; + +export default SettingsButton; diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js new file mode 100644 index 0000000000..05a7db0b9c --- /dev/null +++ b/src/components/views/elements/StartChatButton.js @@ -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 ( + + ); +}; + +StartChatButton.propTypes = { + size: PropTypes.string, + tooltip: PropTypes.bool, +}; + +export default StartChatButton; diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.js index 0ec2c15f0a..d6a8e1fb7e 100644 --- a/src/components/views/elements/TruncatedList.js +++ b/src/components/views/elements/TruncatedList.js @@ -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 ( -
And {overflowCount} more...
+
{_t("And %(count)s more...", {count: overflowCount})}
); } }; diff --git a/src/components/views/elements/UserSelector.js b/src/components/views/elements/UserSelector.js index 266e10154f..955903aac0 100644 --- a/src/components/views/elements/UserSelector.js +++ b/src/components/views/elements/UserSelector.js @@ -16,7 +16,8 @@ limitations under the License. 'use strict'; -var React = require('react'); +import React from 'react'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'UserSelector', @@ -59,9 +60,9 @@ module.exports = React.createClass({ return
  • {user_id} - X
  • ; })} - +
    ); diff --git a/src/components/views/login/CaptchaForm.js b/src/components/views/login/CaptchaForm.js index 0977f947aa..b0dd29582b 100644 --- a/src/components/views/login/CaptchaForm.js +++ b/src/components/views/login/CaptchaForm.js @@ -16,7 +16,9 @@ limitations under the License. 'use strict'; -var React = require('react'); +import React from 'react'; +import { _t } from '../../../languageHandler'; + var DIV_ID = 'mx_recaptcha'; /** @@ -117,7 +119,7 @@ module.exports = React.createClass({ return (
    - This Home Server would like to make sure you are not a robot + {_t("This Home Server would like to make sure you are not a robot")}
    {error} diff --git a/src/components/views/login/CasLogin.js b/src/components/views/login/CasLogin.js index c818586d52..96e37875be 100644 --- a/src/components/views/login/CasLogin.js +++ b/src/components/views/login/CasLogin.js @@ -16,7 +16,8 @@ limitations under the License. 'use strict'; -var React = require('react'); +import React from 'react'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'CasLogin', @@ -28,7 +29,7 @@ module.exports = React.createClass({ render: function() { return (
    - +
    ); } diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index 9729c9e23f..7024db339c 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -19,7 +19,6 @@ import React from 'react'; import sdk from '../../../index'; import { COUNTRIES } from '../../../phonenumber'; -import { charactersToImageNode } from '../../../HtmlUtils'; const COUNTRIES_BY_ISO2 = new Object(null); for (const c of COUNTRIES) { @@ -27,22 +26,27 @@ for (const c of COUNTRIES) { } function countryMatchesSearchQuery(query, country) { + // Remove '+' if present (when searching for a prefix) + if (query[0] === '+') { + query = query.slice(1); + } + if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; if (country.iso2 == query.toUpperCase()) return true; - if (country.prefix == query) return true; + if (country.prefix.indexOf(query) !== -1) return true; return false; } -const MAX_DISPLAYED_ROWS = 2; - export default class CountryDropdown extends React.Component { constructor(props) { super(props); this._onSearchChange = this._onSearchChange.bind(this); + this._onOptionChange = this._onOptionChange.bind(this); + this._getShortOption = this._getShortOption.bind(this); this.state = { searchQuery: '', - } + }; } componentWillMount() { @@ -50,7 +54,7 @@ export default class CountryDropdown extends React.Component { // If no value is given, we start with the first // country selected, but our parent component // doesn't know this, therefore we do this. - this.props.onOptionChange(COUNTRIES[0].iso2); + this.props.onOptionChange(COUNTRIES[0]); } } @@ -60,14 +64,26 @@ export default class CountryDropdown extends React.Component { }); } + _onOptionChange(iso2) { + this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]); + } + _flagImgForIso2(iso2) { - // Unicode Regional Indicator Symbol letter 'A' - const RIS_A = 0x1F1E6; - const ASCII_A = 65; - return charactersToImageNode(iso2, - RIS_A + (iso2.charCodeAt(0) - ASCII_A), - RIS_A + (iso2.charCodeAt(1) - ASCII_A), - ); + return ; + } + + _getShortOption(iso2) { + if (!this.props.isSmall) { + return undefined; + } + let countryPrefix; + if (this.props.showPrefix) { + countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix; + } + return + { this._flagImgForIso2(iso2) } + { countryPrefix } + ; } render() { @@ -93,14 +109,10 @@ export default class CountryDropdown extends React.Component { displayedCountries = COUNTRIES; } - if (displayedCountries.length > MAX_DISPLAYED_ROWS) { - displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS); - } - const options = displayedCountries.map((country) => { return
    {this._flagImgForIso2(country.iso2)} - {country.name} + {country.name} (+{country.prefix})
    ; }); @@ -108,18 +120,21 @@ export default class CountryDropdown extends React.Component { // values between mounting and the initial value propgating const value = this.props.value || COUNTRIES[0].iso2; - return {options} - + ; } } CountryDropdown.propTypes = { className: React.PropTypes.string, + isSmall: React.PropTypes.bool, + // if isSmall, show +44 in the selected value + showPrefix: React.PropTypes.bool, onOptionChange: React.PropTypes.func.isRequired, value: React.PropTypes.string, }; diff --git a/src/components/views/login/CustomServerDialog.js b/src/components/views/login/CustomServerDialog.js index e6450adef1..f5c5c84e63 100644 --- a/src/components/views/login/CustomServerDialog.js +++ b/src/components/views/login/CustomServerDialog.js @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require("react"); +import React from 'react'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'CustomServerDialog', @@ -23,24 +24,24 @@ module.exports = React.createClass({ return (
    - Custom Server Options + {_t("Custom Server Options")}
    - You can use the custom server options to sign into other Matrix - servers by specifying a different Home server URL. + {_t("You can use the custom server options to sign into other Matrix " + + "servers by specifying a different Home server URL.")}
    - This allows you to use this app with an existing Matrix account on - a different home server. + {_t("This allows you to use this app with an existing Matrix account on " + + "a different home server.")}

    - You can also set a custom identity server but this will typically prevent - interaction with users based on email address. + {_t("You can also set a custom identity server but this will typically prevent " + + "interaction with users based on email address.")}
    diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index c4084facb2..ae8a087fdd 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -20,6 +20,7 @@ import url from 'url'; import classnames from 'classnames'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -128,8 +129,8 @@ export const PasswordAuthEntry = React.createClass({ return (
    -

    To continue, please enter your password.

    -

    Password:

    +

    {_t("To continue, please enter your password.")}

    +

    {_t("Password:")}

    -

    An email has been sent to {this.props.inputs.emailAddress}

    -

    Please check your email to continue registration.

    +

    {_t("An email has been sent to")} {this.props.inputs.emailAddress}

    +

    {_t("Please check your email to continue registration.")}

    ); } @@ -348,7 +349,7 @@ export const MsisdnAuthEntry = React.createClass({ }); } else { this.setState({ - errorText: "Token incorrect", + errorText: _t("Token incorrect"), }); } }).catch((e) => { @@ -369,8 +370,8 @@ export const MsisdnAuthEntry = React.createClass({ }); return (
    -

    A text message has been sent to +{this._msisdn}

    -

    Please enter the code it contains:

    +

    {_t("A text message has been sent to")} +{this._msisdn}

    +

    {_t("Please enter the code it contains:")}


    - @@ -439,7 +440,7 @@ export const FallbackAuthEntry = React.createClass({ render: function() { return (
    - Start authentication + {_t("Start authentication")}
    {this.props.errorText}
    diff --git a/src/components/views/login/LoginFooter.js b/src/components/views/login/LoginFooter.js index 5ec57194e0..8bdec71685 100644 --- a/src/components/views/login/LoginFooter.js +++ b/src/components/views/login/LoginFooter.js @@ -16,7 +16,8 @@ limitations under the License. 'use strict'; -var React = require('react'); +import { _t } from '../../../languageHandler'; +import React from 'react'; module.exports = React.createClass({ displayName: 'LoginFooter', @@ -24,8 +25,8 @@ module.exports = React.createClass({ render: function() { return ( ); - } + }, }); diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 61cb3da652..46a48d14a0 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -16,64 +16,58 @@ 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'; import {field_input_incorrect} from '../../../UiEffects'; /** * A pure UI component which displays a username/password form. */ -module.exports = React.createClass({displayName: 'PasswordLogin', - propTypes: { - onSubmit: React.PropTypes.func.isRequired, // fn(username, password) - onForgotPasswordClick: React.PropTypes.func, // fn() - initialUsername: React.PropTypes.string, - initialPhoneCountry: React.PropTypes.string, - initialPhoneNumber: React.PropTypes.string, - initialPassword: React.PropTypes.string, - onUsernameChanged: React.PropTypes.func, - onPhoneCountryChanged: React.PropTypes.func, - onPhoneNumberChanged: React.PropTypes.func, - onPasswordChanged: React.PropTypes.func, - loginIncorrect: React.PropTypes.bool, - }, +class PasswordLogin extends React.Component { + static defaultProps = { + onUsernameChanged: function() {}, + onPasswordChanged: function() {}, + onPhoneCountryChanged: function() {}, + onPhoneNumberChanged: function() {}, + initialUsername: "", + initialPhoneCountry: "", + initialPhoneNumber: "", + initialPassword: "", + loginIncorrect: false, + hsDomain: "", + } - getDefaultProps: function() { - return { - onUsernameChanged: function() {}, - onPasswordChanged: function() {}, - onPhoneCountryChanged: function() {}, - onPhoneNumberChanged: function() {}, - initialUsername: "", - initialPhoneCountry: "", - initialPhoneNumber: "", - initialPassword: "", - loginIncorrect: false, - }; - }, - - getInitialState: function() { - return { + constructor(props) { + super(props); + this.state = { username: this.props.initialUsername, password: this.props.initialPassword, phoneCountry: this.props.initialPhoneCountry, phoneNumber: this.props.initialPhoneNumber, + loginType: PasswordLogin.LOGIN_FIELD_MXID, }; - }, - componentWillMount: function() { + this.onSubmitForm = this.onSubmitForm.bind(this); + this.onUsernameChanged = this.onUsernameChanged.bind(this); + this.onLoginTypeChange = this.onLoginTypeChange.bind(this); + this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this); + this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this); + this.onPasswordChanged = this.onPasswordChanged.bind(this); + } + + componentWillMount() { this._passwordField = null; - }, + } - componentWillReceiveProps: function(nextProps) { + componentWillReceiveProps(nextProps) { if (!this.props.loginIncorrect && nextProps.loginIncorrect) { field_input_incorrect(this._passwordField); } - }, + } - onSubmitForm: function(ev) { + onSubmitForm(ev) { ev.preventDefault(); this.props.onSubmit( this.state.username, @@ -81,35 +75,95 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.state.phoneNumber, this.state.password, ); - }, + } - onUsernameChanged: function(ev) { + onUsernameChanged(ev) { this.setState({username: ev.target.value}); this.props.onUsernameChanged(ev.target.value); - }, + } - onPhoneCountryChanged: function(country) { - this.setState({phoneCountry: country}); - this.props.onPhoneCountryChanged(country); - }, + onLoginTypeChange(loginType) { + this.setState({ + loginType: loginType, + username: "" // Reset because email and username use the same state + }); + } - onPhoneNumberChanged: function(ev) { + onPhoneCountryChanged(country) { + this.setState({ + phoneCountry: country.iso2, + phonePrefix: country.prefix, + }); + this.props.onPhoneCountryChanged(country.iso2); + } + + onPhoneNumberChanged(ev) { this.setState({phoneNumber: ev.target.value}); this.props.onPhoneNumberChanged(ev.target.value); - }, + } - onPasswordChanged: function(ev) { + onPasswordChanged(ev) { this.setState({password: ev.target.value}); this.props.onPasswordChanged(ev.target.value); - }, + } - render: function() { + renderLoginField(loginType) { + switch(loginType) { + case PasswordLogin.LOGIN_FIELD_EMAIL: + return ; + case PasswordLogin.LOGIN_FIELD_MXID: + return ; + case PasswordLogin.LOGIN_FIELD_PHONE: + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + return
    + + +
    ; + } + } + + render() { var forgotPasswordJsx; if (this.props.onForgotPasswordClick) { forgotPasswordJsx = ( - Forgot your password? + { _t('Forgot your password?') } ); } @@ -119,38 +173,54 @@ module.exports = React.createClass({displayName: 'PasswordLogin', error: this.props.loginIncorrect, }); - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + const Dropdown = sdk.getComponent('elements.Dropdown'); + + const loginField = this.renderLoginField(this.state.loginType); + return (
    - - or -
    - - +
    + + + { _t('my Matrix ID') } + { _t('Email address') } + { _t('Phone') } +
    -
    + {loginField} {this._passwordField = e;}} type="password" name="password" value={this.state.password} onChange={this.onPasswordChanged} - placeholder="Password" /> + placeholder={ _t('Password') } />
    {forgotPasswordJsx} - +
    ); } -}); +} + +PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email"; +PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid"; +PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone"; + +PasswordLogin.propTypes = { + onSubmit: React.PropTypes.func.isRequired, // fn(username, password) + onForgotPasswordClick: React.PropTypes.func, // fn() + initialUsername: React.PropTypes.string, + initialPhoneCountry: React.PropTypes.string, + initialPhoneNumber: React.PropTypes.string, + initialPassword: React.PropTypes.string, + onUsernameChanged: React.PropTypes.func, + onPhoneCountryChanged: React.PropTypes.func, + onPhoneNumberChanged: React.PropTypes.func, + onPasswordChanged: React.PropTypes.func, + loginIncorrect: React.PropTypes.bool, +}; + +module.exports = PasswordLogin; diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 4868c9de63..0ca4615a84 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -21,6 +21,7 @@ import sdk from '../../../index'; import Email from '../../../email'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; +import { _t } from '../../../languageHandler'; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_COUNTRY = 'field_phone_country'; @@ -100,27 +101,26 @@ module.exports = React.createClass({ if (this.refs.email.value == '') { var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createDialog(QuestionDialog, { - title: "Warning", + title: _t("Warning!"), description:
    - If you don't specify an email address, you won't be able to reset your password.
    - Are you sure? + {_t("If you don't specify an email address, you won't be able to reset your password. " + + "Are you sure?")}
    , - button: "Continue", + 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, @@ -270,7 +270,8 @@ module.exports = React.createClass({ _onPhoneCountryChange(newVal) { this.setState({ - phoneCountry: newVal, + phoneCountry: newVal.iso2, + phonePrefix: newVal.prefix, }); }, @@ -280,7 +281,7 @@ module.exports = React.createClass({ const emailSection = (
    - You are registering with {this.state.selectedTeam.name} + {_t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name})}

    ); } @@ -313,14 +314,19 @@ module.exports = React.createClass({ const phoneSection = (
    + ); - let placeholderUserName = "User name"; + let placeholderUserName = _t("User name"); if (this.props.guestUsername) { - placeholderUserName += " (default: " + this.props.guestUsername + ")"; + placeholderUserName += " " + _t("(default: %(userName)s)", {userName: this.props.guestUsername}); } return ( @@ -349,15 +355,15 @@ module.exports = React.createClass({ onBlur={function() {self.validateField(FIELD_USERNAME);}} />
    { this.props.guestUsername ? -
    Setting a user name will create a fresh account
    : null +
    {_t("Setting a user name will create a fresh account")}
    : null } + placeholder={_t("Password")} defaultValue={this.props.defaultPassword} />
    diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index 4e6ed12f9e..a63d02416c 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -19,6 +19,7 @@ limitations under the License. var React = require('react'); var Modal = require('../../../Modal'); var sdk = require('../../../index'); +import { _t } from '../../../languageHandler'; /** * A pure UI component which displays the HS and IS to use. @@ -27,8 +28,7 @@ module.exports = React.createClass({ displayName: 'ServerConfig', propTypes: { - onHsUrlChanged: React.PropTypes.func, - onIsUrlChanged: React.PropTypes.func, + onServerConfigChange: React.PropTypes.func, // default URLs are defined in config.json (or the hardcoded defaults) // they are used if the user has not overridden them with a custom URL. @@ -50,8 +50,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - onHsUrlChanged: function() {}, - onIsUrlChanged: function() {}, + onServerConfigChange: function() {}, customHsUrl: "", customIsUrl: "", withToggleButton: false, @@ -75,7 +74,10 @@ module.exports = React.createClass({ this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { var hsUrl = this.state.hs_url.trim().replace(/\/$/, ""); if (hsUrl === "") hsUrl = this.props.defaultHsUrl; - this.props.onHsUrlChanged(hsUrl); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); }); }); }, @@ -85,7 +87,10 @@ module.exports = React.createClass({ this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() { var isUrl = this.state.is_url.trim().replace(/\/$/, ""); if (isUrl === "") isUrl = this.props.defaultIsUrl; - this.props.onIsUrlChanged(isUrl); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); }); }); }, @@ -102,12 +107,16 @@ module.exports = React.createClass({ configVisible: visible }); if (!visible) { - this.props.onHsUrlChanged(this.props.defaultHsUrl); - this.props.onIsUrlChanged(this.props.defaultIsUrl); + this.props.onServerConfigChange({ + hsUrl : this.props.defaultHsUrl, + isUrl : this.props.defaultIsUrl, + }); } else { - this.props.onHsUrlChanged(this.state.hs_url); - this.props.onIsUrlChanged(this.state.is_url); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); } }, @@ -123,19 +132,19 @@ module.exports = React.createClass({ var toggleButton; if (this.props.withToggleButton) { toggleButton = ( -
    +
      
    ); @@ -147,7 +156,7 @@ module.exports = React.createClass({
    - What does this mean? + {_t("What does this mean?")}
    diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 73b9bdb200..52c1341e60 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -20,8 +20,8 @@ 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'; export default class MAudioBody extends React.Component { constructor(props) { @@ -77,7 +77,7 @@ export default class MAudioBody extends React.Component { return ( - Error decrypting audio + {_t("Error decrypting audio")} ); } diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 86aee28269..bccae923eb 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -20,10 +20,10 @@ import React from 'react'; import filesize from 'filesize'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; +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'; @@ -202,7 +202,7 @@ module.exports = React.createClass({ * @return {string} the human readable link text for the attachment. */ presentableTextForFile: function(content) { - var linkText = 'Attachment'; + var linkText = _t("Attachment"); if (content.body && content.body.length > 0) { // The content body should be the name of the file including a // file extension. @@ -261,7 +261,7 @@ module.exports = React.createClass({ const content = this.props.mxEvent.getContent(); const text = this.presentableTextForFile(content); const isEncrypted = content.file !== undefined; - const fileName = content.body && content.body.length > 0 ? content.body : "Attachment"; + const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment"); const contentUrl = this._getContentUrl(); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -283,7 +283,8 @@ module.exports = React.createClass({ }).catch((err) => { console.warn("Unable to decrypt attachment: ", err); Modal.createDialog(ErrorDialog, { - description: "Error decrypting attachment" + title: _t("Error"), + description: _t("Error decrypting attachment"), }); }).finally(() => { decrypting = false; @@ -295,7 +296,7 @@ module.exports = React.createClass({ @@ -314,7 +315,7 @@ module.exports = React.createClass({ // We can't provide a Content-Disposition header like we would for HTTP. download: fileName, target: "_blank", - textContent: "Download " + text, + textContent: _t("Download %(text)s", { text: text }), }, "*"); }; @@ -346,7 +347,7 @@ module.exports = React.createClass({ return (
    - + { fileName }
    @@ -360,9 +361,9 @@ module.exports = React.createClass({ return ( @@ -371,7 +372,7 @@ module.exports = React.createClass({ } else { var extra = text ? (': ' + text) : ''; return - Invalid file{extra} + { _t("Invalid file%(extra)s", { extra: extra }) } ; } }, diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index ab163297d7..da6447c7e5 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -26,6 +26,7 @@ import dis from '../../../dispatcher'; import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile'; import q from 'q'; import UserSettingsStore from '../../../UserSettingsStore'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'MImageBody', @@ -56,6 +57,7 @@ module.exports = React.createClass({ const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: httpUrl, + name: content.body && content.body.length > 0 ? content.body : _t('Attachment'), mxEvent: this.props.mxEvent, }; @@ -190,7 +192,7 @@ module.exports = React.createClass({ return ( - Error decrypting image + {_t("Error decrypting image")} ); } @@ -237,13 +239,13 @@ module.exports = React.createClass({ } else if (content.body) { return ( - Image '{content.body}' cannot be displayed. + {_t("Image '%(Body)s' cannot be displayed.", {Body: content.body})} ); } else { return ( - This image cannot be displayed. + {_t("This image cannot be displayed.")} ); } diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index d843115caf..46d8366592 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -19,11 +19,10 @@ 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'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'MVideoBody', @@ -128,7 +127,7 @@ module.exports = React.createClass({ return ( - Error decrypting video + {_t("Error decrypting video")} ); } diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js new file mode 100644 index 0000000000..525f7b81ee --- /dev/null +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -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 ( +
    + { _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) } +
    + ); + } + + 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 ( +
    + { _tJsx('$senderDisplayName changed the room avatar to ', + [ + /\$senderDisplayName/, + //, + ], + [ + (sub) => senderDisplayName, + (sub) => + + + , + ] + ) + } +
    + ); + }, +}); diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 9e6fba2127..e224714a27 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -30,7 +30,7 @@ export default function SenderProfile(props) { } return ( - {`${name || ''} ${props.aux || ''}`} ); } diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 9aebb30f54..d5a1977cdd 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -28,6 +28,7 @@ import ScalarAuthClient from '../../../ScalarAuthClient'; import Modal from '../../../Modal'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher'; +import { _t } from '../../../languageHandler'; linkifyMatrix(linkify); @@ -62,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; @@ -80,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); + }; + } } }, @@ -132,7 +154,8 @@ module.exports = React.createClass({ links.push(node); } } - else if (node.tagName === "PRE" || node.tagName === "CODE") { + else if (node.tagName === "PRE" || node.tagName === "CODE" || + node.tagName === "BLOCKQUOTE") { continue; } else if (node.children && node.children.length) { @@ -229,14 +252,14 @@ module.exports = React.createClass({ let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); let integrationsUrl = SdkConfig.get().integrations_ui_url; Modal.createDialog(QuestionDialog, { - title: "Add an Integration", + title: _t("Add an Integration"), description:
    - You are about to be taken to a third-party site so you can - authenticate your account for use with {integrationsUrl}.
    - Do you wish to continue? + {_t("You are about to be taken to a third-party site so you can " + + "authenticate your account for use with %(integrationsUrl)s. " + + "Do you wish to continue?", { integrationsUrl: integrationsUrl })}
    , - button: "Continue", + button: _t("Continue"), onFinished: function(confirmed) { if (!confirmed) { return; diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.js index 8319dbd434..088f7cbbc6 100644 --- a/src/components/views/messages/TextualEvent.js +++ b/src/components/views/messages/TextualEvent.js @@ -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); diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 9b1bd74087..1b6f6426e5 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -16,7 +16,8 @@ limitations under the License. 'use strict'; -var React = require('react'); +import React from 'react'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'UnknownBody', @@ -24,7 +25,7 @@ module.exports = React.createClass({ render: function() { const text = this.props.mxEvent.getContent().body; return ( - + {text} ); diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 6543f2a17d..b9c9c7d989 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -19,6 +19,7 @@ var React = require('react'); var ObjectUtils = require("../../../ObjectUtils"); var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require("../../../index"); +import { _t } from '../../../languageHandler'; var Modal = require("../../../Modal"); module.exports = React.createClass({ @@ -154,8 +155,8 @@ module.exports = React.createClass({ else { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Invalid alias format", - description: "'" + alias + "' is not a valid format for an alias", + title: _t('Invalid alias format'), + description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }), }); } }, @@ -170,8 +171,8 @@ module.exports = React.createClass({ else { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Invalid address format", - description: "'" + alias + "' is not a valid format for an address", + title: _t('Invalid address format'), + description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }), }); } }, @@ -203,7 +204,7 @@ module.exports = React.createClass({ if (this.props.canSetCanonicalAlias) { canonical_alias_section = ( - Disable URL previews by default for participants in this room + {_t("Disable URL previews by default for participants in this room")} ; } else { disableRoomPreviewUrls = ; } + let urlPreviewText = null; + if (UserSettingsStore.getUrlPreviewsDisabled()) { + urlPreviewText = ( + _tJsx("You have disabled URL previews by default.", /(.*?)<\/a>/, (sub)=>{sub}) + ); + } + else { + urlPreviewText = ( + _tJsx("You have enabled URL previews by default.", /(.*?)<\/a>/, (sub)=>{sub}) + ); + } + return (
    -

    URL Previews

    +

    {_t("URL Previews")}

    { disableRoomPreviewUrls }
    ); diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 9be91e068a..5f6bac0007 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -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'; diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index ec2de2558c..efa4da141f 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -14,12 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -const React = require('react'); -const MatrixClientPeg = require("../../../MatrixClientPeg"); -const sdk = require('../../../index'); -const dis = require("../../../dispatcher"); -const ObjectUtils = require('../../../ObjectUtils'); -const AppsDrawer = require('./AppsDrawer'); +import React from 'react'; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import sdk from '../../../index'; +import dis from "../../../dispatcher"; +import ObjectUtils from '../../../ObjectUtils'; +import AppsDrawer from './AppsDrawer'; +import { _t, _tJsx} from '../../../languageHandler'; + module.exports = React.createClass({ displayName: 'AuxPanel', @@ -77,10 +79,10 @@ module.exports = React.createClass({ fileDropTarget = (
    + title={_t("Drop File Here")}>
    - Drop file here to upload + {_t("Drop file here to upload")}
    ); @@ -91,17 +93,22 @@ module.exports = React.createClass({ let supportedText; let joinText; if (!MatrixClientPeg.get().supportsVoip()) { - supportedText = " (unsupported)"; + supportedText = _t(" (unsupported)"); } else { joinText = ( - Join as { this.onConferenceNotificationClick(event, 'voice');}} - href="#">voice or { this.onConferenceNotificationClick(event, 'video'); }} - href="#">video. + {_tJsx( + "Join as voice or video.", + [/(.*?)<\/voiceText>/, /(.*?)<\/videoText>/], + [ + (sub) => { this.onConferenceNotificationClick(event, 'voice');}} href="#">{sub}, + (sub) => { this.onConferenceNotificationClick(event, 'video');}} href="#">{sub}, + ] + )} ); } conferenceCallNotification = (
    - Ongoing conference call{ supportedText }. { joinText } + {_t("Ongoing conference call%(supportedText)s. %(joinText)s", {supportedText: supportedText, joinText: joinText})}
    ); } diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 71e8fb0be7..7002e45c3b 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -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 = (
    - {name} + {name} @@ -124,7 +125,7 @@ module.exports = React.createClass({ } else { nameEl = ( - {name} + {name} ); } @@ -140,10 +141,10 @@ module.exports = React.createClass({ var power; var powerLevel = this.props.powerLevel; if (powerLevel >= 50 && powerLevel < 99) { - power = Mod; + power = {_t("Moderator")}/; } if (powerLevel >= 99) { - power = Admin; + power = {_t("Admin")}/; } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 8b8e52ae83..80f062aef7 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -16,8 +16,10 @@ limitations under the License. 'use strict'; + var React = require('react'); var classNames = require("classnames"); +import { _t } from '../../../languageHandler'; var Modal = require('../../../Modal'); var sdk = require('../../../index'); @@ -36,10 +38,12 @@ 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', 'm.room.encryption' : 'messages.TextualEvent', + 'm.room.power_levels' : 'messages.TextualEvent', }; var MAX_READ_AVATARS = 5; @@ -128,6 +132,9 @@ module.exports = WithMatrixClient(React.createClass({ * for now. */ tileShape: React.PropTypes.string, + + // show twelve hour timestamps + isTwelveHour: React.PropTypes.bool, }, getInitialState: function() { @@ -283,21 +290,17 @@ module.exports = WithMatrixClient(React.createClass({ }, getReadAvatars: function() { + + // return early if there are no read receipts + if (!this.props.readReceipts || this.props.readReceipts.length === 0) { + return (); + } + const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); const avatars = []; const receiptOffset = 15; let left = 0; - // It's possible that the receipt was sent several days AFTER the event. - // If it is, we want to display the complete date along with the HH:MM:SS, - // rather than just HH:MM:SS. - let dayAfterEvent = new Date(this.props.mxEvent.getTs()); - dayAfterEvent.setDate(dayAfterEvent.getDate() + 1); - dayAfterEvent.setHours(0); - dayAfterEvent.setMinutes(0); - dayAfterEvent.setSeconds(0); - let dayAfterEventTime = dayAfterEvent.getTime(); - var receipts = this.props.readReceipts || []; for (var i = 0; i < receipts.length; ++i) { var receipt = receipts[i]; @@ -333,7 +336,6 @@ module.exports = WithMatrixClient(React.createClass({ suppressAnimation={this._suppressReadReceiptAnimation} onClick={this.toggleAllReadAvatars} timestamp={receipt.ts} - showFullTimestamp={receipt.ts >= dayAfterEventTime} /> ); } @@ -394,8 +396,7 @@ module.exports = WithMatrixClient(React.createClass({ var msgtype = content.msgtype; var eventType = this.props.mxEvent.getType(); - // Info messages are basically information about commands processed on a - // room, or emote messages + // Info messages are basically information about commands processed on a room var isInfoMessage = (eventType !== 'm.room.message'); var EventTileType = sdk.getComponent(eventTileTypes[eventType]); @@ -409,9 +410,10 @@ module.exports = WithMatrixClient(React.createClass({ var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted; - var classes = classNames({ + const classes = classNames({ mx_EventTile: true, mx_EventTile_info: isInfoMessage, + mx_EventTile_12hr: this.props.isTwelveHour, mx_EventTile_encrypting: this.props.eventSendStatus == 'encrypting', mx_EventTile_sending: isSending, mx_EventTile_notSent: this.props.eventSendStatus == 'not_sent', @@ -423,7 +425,8 @@ module.exports = WithMatrixClient(React.createClass({ menu: this.state.menu, mx_EventTile_verified: this.state.verified == true, mx_EventTile_unverified: this.state.verified == false, - mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted', + mx_EventTile_bad: msgtype === 'm.bad.encrypted', + mx_EventTile_emote: msgtype === 'm.emote', mx_EventTile_redacted: isRedacted, }); @@ -468,9 +471,9 @@ module.exports = WithMatrixClient(React.createClass({ if (needsSenderProfile) { let aux = null; if (!this.props.tileShape) { - if (msgtype === 'm.image') aux = "sent an image"; - else if (msgtype === 'm.video') aux = "sent a video"; - else if (msgtype === 'm.file') aux = "uploaded a file"; + if (msgtype === 'm.image') aux = _t('sent an image'); + else if (msgtype === 'm.video') aux = _t('sent a video'); + else if (msgtype === 'm.file') aux = _t('uploaded a file'); sender = ; } else { @@ -478,36 +481,34 @@ module.exports = WithMatrixClient(React.createClass({ } } - var editButton = ( - + const editButton = ( + ); - - var e2e; + let e2e; // cosmetic padlocks: if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') { - e2e = ; + e2e = {_t("Encrypted; } // real padlocks else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) { if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') { - e2e = ; + e2e = {_t("Undecryptable")}; } else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) { - e2e = ; + e2e = {_t("Encrypted; } else { - e2e = ; + e2e = {_t("Encrypted; } } else if (e2eEnabled) { - e2e = ; + e2e = {_t("Unencrypted; } const timestamp = this.props.mxEvent.getTs() ? - : null; + : null; if (this.props.tileShape === "notif") { - var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); - + const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); return (
    diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js new file mode 100644 index 0000000000..33df201d7c --- /dev/null +++ b/src/components/views/rooms/ForwardMessage.js @@ -0,0 +1,96 @@ +/* + Copyright 2017 Vector Creations Ltd + Copyright 2017 Michael Telatynski + + 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 MatrixClientPeg from '../../../MatrixClientPeg'; +import dis from '../../../dispatcher'; +import KeyCode from '../../../KeyCode'; + + +module.exports = React.createClass({ + displayName: 'ForwardMessage', + + propTypes: { + currentRoomId: React.PropTypes.string.isRequired, + + /* the MatrixEvent to be forwarded */ + mxEvent: React.PropTypes.object.isRequired, + + onCancelClick: React.PropTypes.func.isRequired, + }, + + componentWillMount: function() { + dis.dispatch({ + action: 'ui_opacity', + leftOpacity: 1.0, + rightOpacity: 0.3, + middleOpacity: 0.5, + }); + }, + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + document.addEventListener('keydown', this._onKeyDown); + }, + + componentWillUnmount: function() { + dis.dispatch({ + action: 'ui_opacity', + sideOpacity: 1.0, + middleOpacity: 1.0, + }); + dis.unregister(this.dispatcherRef); + document.removeEventListener('keydown', this._onKeyDown); + }, + + onAction: function(payload) { + if (payload.action === 'view_room') { + const event = this.props.mxEvent; + const Client = MatrixClientPeg.get(); + Client.sendEvent(payload.room_id, event.getType(), event.getContent()).done(() => { + dis.dispatch({action: 'message_sent'}); + }, (err) => { + if (err.name === "UnknownDeviceError") { + dis.dispatch({ + action: 'unknown_device_error', + err: err, + room: Client.getRoom(payload.room_id), + }); + } + dis.dispatch({action: 'message_send_failed'}); + }); + if (this.props.currentRoomId === payload.room_id) this.props.onCancelClick(); + } + }, + + _onKeyDown: function(ev) { + switch (ev.keyCode) { + case KeyCode.ESCAPE: + this.props.onCancelClick(); + break; + } + }, + + render: function() { + return ( +
    +

    {_t('Please select the destination room for this message')}

    +
    + ); + }, +}); diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index ef8fb29cbc..35e6d28b1f 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -100,7 +100,9 @@ module.exports = React.createClass({ render: function() { var p = this.state.preview; - if (!p) return
    ; + if (!p || Object.keys(p).length === 0) { + return
    ; + } // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing? var image = p["og:image"]; diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js index d4c00dda76..33b919835c 100644 --- a/src/components/views/rooms/MemberDeviceInfo.js +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -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 = (
    - Blacklisted + {_t("Blacklisted")}/
    ); } else if (this.props.device.isVerified()) { indicator = (
    - Verified + {_t("Verified")}/
    ); } else { indicator = (
    - Unverified + {_t("Unverified")}/
    ); } @@ -49,7 +50,7 @@ export default class MemberDeviceInfo extends React.Component { // add the deviceId as a titletext to help with debugging return (
    + title={_t("device id: ") + this.props.device.deviceId} >
    {deviceName} diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1459ad3eb7..ff684aeae4 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -31,6 +31,7 @@ import classNames from 'classnames'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; import createRoom from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import Unread from '../../../Unread'; @@ -219,7 +220,7 @@ module.exports = WithMatrixClient(React.createClass({ onKick: function() { const membership = this.props.member.membership; - const kickLabel = membership === "invite" ? "Disinvite" : "Kick"; + const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createDialog(ConfirmUserActionDialog, { member: this.props.member, @@ -241,8 +242,8 @@ module.exports = WithMatrixClient(React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Kick error: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to kick user", + title: _t("Failed to kick"), + description: ((err && err.message) ? err.message : "Operation failed"), }); } ).finally(()=>{ @@ -256,7 +257,7 @@ module.exports = WithMatrixClient(React.createClass({ const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createDialog(ConfirmUserActionDialog, { member: this.props.member, - action: this.props.member.membership == 'ban' ? 'Unban' : 'Ban', + action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"), askReason: this.props.member.membership != 'ban', danger: this.props.member.membership != 'ban', onFinished: (proceed, reason) => { @@ -283,8 +284,8 @@ module.exports = WithMatrixClient(React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Ban error: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to ban user", + title: _t("Error"), + description: _t("Failed to ban user"), }); } ).finally(()=>{ @@ -333,8 +334,8 @@ module.exports = WithMatrixClient(React.createClass({ }, function(err) { console.error("Mute error: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to mute user", + title: _t("Error"), + description: _t("Failed to mute user"), }); } ).finally(()=>{ @@ -374,16 +375,12 @@ 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: "Please Register", - description: "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, { - title: "Error", - description: "Failed to toggle moderator status", + title: _t("Error"), + description: _t("Failed to toggle moderator status"), }); } } @@ -403,8 +400,8 @@ module.exports = WithMatrixClient(React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to change power level " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to change power level", + title: _t("Error"), + description: _t("Failed to change power level"), }); } ).finally(()=>{ @@ -432,13 +429,13 @@ module.exports = WithMatrixClient(React.createClass({ if (parseInt(myPower) === parseInt(powerLevel)) { var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createDialog(QuestionDialog, { - title: "Warning", + title: _t("Warning!"), description:
    - You will not be able to undo this change as you are promoting the user to have the same power level as yourself.
    - Are you sure? + { _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }
    + { _t("Are you sure?") }
    , - button: "Continue", + button: _t("Continue"), onFinished: function(confirmed) { if (confirmed) { self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); @@ -581,9 +578,9 @@ module.exports = WithMatrixClient(React.createClass({ // still loading devComponents = ; } else if (devices === null) { - devComponents = "Unable to load device list"; + devComponents = _t("Unable to load device list"); } else if (devices.length === 0) { - devComponents = "No devices with registered encryption keys"; + devComponents = _t("No devices with registered encryption keys"); } else { devComponents = []; for (var i = 0; i < devices.length; i++) { @@ -595,7 +592,7 @@ module.exports = WithMatrixClient(React.createClass({ return (
    -

    Devices

    +

    { _t("Devices") }

    {devComponents}
    @@ -644,11 +641,11 @@ module.exports = WithMatrixClient(React.createClass({
    -
    Start new chat
    +
    { _t("Start a chat") }
    ; startChat =
    -

    Direct chats

    +

    { _t("Direct chats") }

    {tiles} {startNewChat}
    ; @@ -661,7 +658,7 @@ module.exports = WithMatrixClient(React.createClass({ if (this.state.can.kick) { const membership = this.props.member.membership; - const kickLabel = membership === "invite" ? "Disinvite" : "Kick"; + const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick"); kickButton = ( @@ -670,9 +667,9 @@ module.exports = WithMatrixClient(React.createClass({ ); } if (this.state.can.ban) { - let label = 'Ban'; + let label = _t("Ban"); if (this.props.member.membership == 'ban') { - label = 'Unban'; + label = _t("Unban"); } banButton = ( @@ -691,7 +688,7 @@ module.exports = WithMatrixClient(React.createClass({ ); } if (this.state.can.toggleMod) { - var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator"; + var giveOpLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator"); giveModButton = {giveOpLabel} ; @@ -704,7 +701,7 @@ module.exports = WithMatrixClient(React.createClass({ if (kickButton || banButton || muteButton || giveModButton) { adminTools =
    -

    Admin tools

    +

    {_t("Admin tools")}

    {muteButton} @@ -717,8 +714,16 @@ module.exports = WithMatrixClient(React.createClass({ const memberName = this.props.member.name; + if (this.props.member.user) { + var presenceState = this.props.member.user.presence; + var presenceLastActiveAgo = this.props.member.user.lastActiveAgo; + var presenceLastTs = this.props.member.user.lastPresenceTs; + var presenceCurrentlyActive = this.props.member.user.currentlyActive; + } + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); + var PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); const EmojiText = sdk.getComponent('elements.EmojiText'); return (
    @@ -734,7 +739,12 @@ module.exports = WithMatrixClient(React.createClass({ { this.props.member.userId }
    - Level: + { _t("Level:") } +
    +
    +
    diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index bd386ed1bb..63737d5fad 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -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'; var classNames = require('classnames'); var Matrix = require("matrix-js-sdk"); var q = require('q'); @@ -27,12 +28,6 @@ var CallHandler = require("../../../CallHandler"); var Invite = require("../../../Invite"); var INITIAL_LOAD_NUM_MEMBERS = 30; -var SHARE_HISTORY_WARNING = - - Newly invited users will see the history of this room.
    - If you'd prefer invited users not to see messages that were sent before they joined,
    - turn off, 'Share message history with new users' in the settings for this room. -
    ; module.exports = React.createClass({ displayName: 'MemberList', @@ -207,7 +202,9 @@ module.exports = React.createClass({ // For now we'll pretend this is any entity. It should probably be a separate tile. var EntityTile = sdk.getComponent("rooms.EntityTile"); var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; + var text = (overflowCount > 1) + ? _t("and %(overflowCount)s others...", { overflowCount: overflowCount }) + : _t("and one other..."); return ( @@ -352,7 +349,7 @@ module.exports = React.createClass({ if (invitedMemberTiles.length > 0) { invitedSection = (
    -

    Invited

    +

    { _t("Invited") }

    {invitedMemberTiles}
    @@ -363,8 +360,8 @@ module.exports = React.createClass({ var inputBox = (
    + onChange={this.onSearchQueryChanged} value={this.state.searchQuery} + placeholder={ _t('Filter room members') } />
    ); diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 5becef9ede..d7e6b4a1ec 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -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() { diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index a90de8cb5c..63f980304e 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -13,14 +13,14 @@ 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. */ -const React = require('react'); - -const CallHandler = require('../../../CallHandler'); -const MatrixClientPeg = require('../../../MatrixClientPeg'); -const Modal = require('../../../Modal'); -const sdk = require('../../../index'); -const dis = require('../../../dispatcher'); -// import Autocomplete from './Autocomplete'; +var React = require('react'); +import { _t } from '../../../languageHandler'; +var CallHandler = require('../../../CallHandler'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var Modal = require('../../../Modal'); +var sdk = require('../../../index'); +var dis = require('../../../dispatcher'); +import Autocomplete from './Autocomplete'; import classNames from 'classnames'; import UserSettingsStore from '../../../UserSettingsStore'; @@ -35,6 +35,7 @@ export default class MessageComposer extends React.Component { this.onShowAppsClick = this.onShowAppsClick.bind(this); this.onHideAppsClick = this.onHideAppsClick.bind(this); this.onUploadFileSelected = this.onUploadFileSelected.bind(this); + this.uploadFiles = this.uploadFiles.bind(this); this.onVoiceCallClick = this.onVoiceCallClick.bind(this); this.onInputContentChanged = this.onInputContentChanged.bind(this); this.onUpArrow = this.onUpArrow.bind(this); @@ -45,6 +46,7 @@ export default class MessageComposer extends React.Component { this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this); this.onInputStateChanged = this.onInputStateChanged.bind(this); this.onEvent = this.onEvent.bind(this); + this.onPageUnload = this.onPageUnload.bind(this); this.state = { autocompleteQuery: '', @@ -52,7 +54,7 @@ export default class MessageComposer extends React.Component { inputState: { style: [], blockType: null, - isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true), + isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false), wordCount: 0, }, showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false), @@ -65,12 +67,21 @@ export default class MessageComposer extends React.Component { // marked as encrypted. // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something. MatrixClientPeg.get().on("event", this.onEvent); + + window.addEventListener('beforeunload', this.onPageUnload); } componentWillUnmount() { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("event", this.onEvent); } + window.removeEventListener('beforeunload', this.onPageUnload); + } + + onPageUnload(event) { + if (this.messageComposerInput) { + this.messageComposerInput.sentHistory.saveLastTextEntry(); + } } onEvent(event) { @@ -81,37 +92,33 @@ export default class MessageComposer extends React.Component { onUploadClick(ev) { if (MatrixClientPeg.get().isGuest()) { - const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); - Modal.createDialog(NeedToRegisterDialog, { - title: "Please Register", - description: "Guest users can't upload files. Please register to upload.", - }); + dis.dispatch({action: 'view_set_mxid'}); return; } this.refs.uploadInput.click(); } - onUploadFileSelected(files, isPasted) { - if (!isPasted) { - files = files.target.files; - } + onUploadFileSelected(files) { + this.uploadFiles(files.target.files); + } - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const TintableSvg = sdk.getComponent("elements.TintableSvg"); + uploadFiles(files) { + let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + let TintableSvg = sdk.getComponent("elements.TintableSvg"); const fileList = []; for (let i=0; i - {files[i].name || 'Attachment'} + {files[i].name || _t('Attachment')} ); } Modal.createDialog(QuestionDialog, { - title: "Upload Files", + title: _t('Upload Files'), description: (
    -

    Are you sure you want upload the following files?

    +

    { _t('Are you sure you want to upload the following files?') }

      {fileList}
    @@ -245,11 +252,11 @@ export default class MessageComposer extends React.Component { if (roomIsEncrypted) { // FIXME: show a /!\ if there are untrusted devices in the room... e2eImg = 'img/e2e-verified.svg'; - e2eTitle = 'Encrypted room'; + e2eTitle = _t('Encrypted room'); e2eClass = 'mx_MessageComposer_e2eIcon'; } else { e2eImg = 'img/e2e-unencrypted.svg'; - e2eTitle = 'Unencrypted room'; + e2eTitle = _t('Unencrypted room'); e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor'; } @@ -262,15 +269,15 @@ export default class MessageComposer extends React.Component { if (this.props.callState && this.props.callState !== 'ended') { hangupButton =
    - Hangup + {
    ; } else { callButton = -
    +
    ; videoCallButton = -
    +
    ; } @@ -297,7 +304,7 @@ export default class MessageComposer extends React.Component { // complex because of conference calls. const uploadButton = (
    + onClick={this.onUploadClick} title={ _t('Upload file') }> , @@ -344,8 +351,8 @@ export default class MessageComposer extends React.Component { } else { controls.push(
    - You do not have permission to post to this room -
    , + { _t('You do not have permission to post to this room') } +
    ); } @@ -373,7 +380,7 @@ export default class MessageComposer extends React.Component { mx_filterFlipColor: true, }); return {formatButtons}
    - - diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 51c9ba881b..b7ef9fa184 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -28,11 +28,12 @@ import Q from 'q'; import MatrixClientPeg from '../../../MatrixClientPeg'; import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; import SlashCommands from '../../../SlashCommands'; +import KeyCode from '../../../KeyCode'; import Modal from '../../../Modal'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher'; -import KeyCode from '../../../KeyCode'; import UserSettingsStore from '../../../UserSettingsStore'; import * as RichText from '../../../RichText'; @@ -44,8 +45,6 @@ import {onSendMessageFailed} from './MessageComposerInputOld'; const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000; -const KEY_M = 77; - const ZWS_CODE = 8203; const ZWS = String.fromCharCode(ZWS_CODE); // zero width space function stateToMarkdown(state) { @@ -61,7 +60,7 @@ function stateToMarkdown(state) { export default class MessageComposerInput extends React.Component { static getKeyBinding(e: SyntheticKeyboardEvent): string { // C-m => Toggles between rich text and markdown modes - if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { + if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { return 'toggle-mode'; } @@ -84,7 +83,6 @@ export default class MessageComposerInput extends React.Component { this.onAction = this.onAction.bind(this); this.handleReturn = this.handleReturn.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this); - this.handlePastedFiles = this.handlePastedFiles.bind(this); this.onEditorContentChanged = this.onEditorContentChanged.bind(this); this.setEditorState = this.setEditorState.bind(this); this.onUpArrow = this.onUpArrow.bind(this); @@ -94,7 +92,7 @@ export default class MessageComposerInput extends React.Component { this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this); this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this); - const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true); + const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false); this.state = { // whether we're in rich text or markdown mode @@ -355,6 +353,7 @@ export default class MessageComposerInput extends React.Component { } sendTyping(isTyping) { + if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return; MatrixClientPeg.get().sendTyping( this.props.room.roomId, this.isTyping, TYPING_SERVER_TIMEOUT @@ -476,10 +475,6 @@ export default class MessageComposerInput extends React.Component { return false; } - handlePastedFiles(files) { - this.props.onUploadFileSelected(files, true); - } - handleReturn(ev) { if (ev.shiftKey) { this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); @@ -508,8 +503,8 @@ export default class MessageComposerInput extends React.Component { console.error("Command failure: %s", err); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Server error", - description: "Server unavailable, overloaded, or something else went wrong.", + title: _t("Server error"), + description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")), }); }); } @@ -517,8 +512,8 @@ export default class MessageComposerInput extends React.Component { console.error(cmd.error); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Command error", - description: cmd.error + title: _t("Command error"), + description: cmd.error, }); } return true; @@ -541,9 +536,9 @@ export default class MessageComposerInput extends React.Component { let sendTextFn = this.client.sendTextMessage; if (contentText.startsWith('/me')) { - contentText = contentText.replace('/me ', ''); + contentText = contentText.substring(4); // bit of a hack, but the alternative would be quite complicated - if (contentHTML) contentHTML = contentHTML.replace('/me ', ''); + if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, ''); sendHtmlFn = this.client.sendHtmlEmote; sendTextFn = this.client.sendEmoteMessage; } @@ -723,9 +718,10 @@ export default class MessageComposerInput extends React.Component {
    -