merge develop
|
@ -2,9 +2,7 @@
|
||||||
|
|
||||||
src/autocomplete/AutocompleteProvider.js
|
src/autocomplete/AutocompleteProvider.js
|
||||||
src/autocomplete/Autocompleter.js
|
src/autocomplete/Autocompleter.js
|
||||||
src/autocomplete/EmojiProvider.js
|
|
||||||
src/autocomplete/UserProvider.js
|
src/autocomplete/UserProvider.js
|
||||||
src/CallHandler.js
|
|
||||||
src/component-index.js
|
src/component-index.js
|
||||||
src/components/structures/BottomLeftMenu.js
|
src/components/structures/BottomLeftMenu.js
|
||||||
src/components/structures/CompatibilityPage.js
|
src/components/structures/CompatibilityPage.js
|
||||||
|
@ -13,27 +11,22 @@ src/components/structures/HomePage.js
|
||||||
src/components/structures/LeftPanel.js
|
src/components/structures/LeftPanel.js
|
||||||
src/components/structures/LoggedInView.js
|
src/components/structures/LoggedInView.js
|
||||||
src/components/structures/login/ForgotPassword.js
|
src/components/structures/login/ForgotPassword.js
|
||||||
src/components/structures/login/Login.js
|
|
||||||
src/components/structures/login/Registration.js
|
|
||||||
src/components/structures/LoginBox.js
|
src/components/structures/LoginBox.js
|
||||||
src/components/structures/MessagePanel.js
|
src/components/structures/MessagePanel.js
|
||||||
src/components/structures/NotificationPanel.js
|
src/components/structures/NotificationPanel.js
|
||||||
src/components/structures/RoomDirectory.js
|
src/components/structures/RoomDirectory.js
|
||||||
src/components/structures/RoomStatusBar.js
|
src/components/structures/RoomStatusBar.js
|
||||||
src/components/structures/RoomSubList.js
|
|
||||||
src/components/structures/RoomView.js
|
src/components/structures/RoomView.js
|
||||||
src/components/structures/ScrollPanel.js
|
src/components/structures/ScrollPanel.js
|
||||||
src/components/structures/SearchBox.js
|
src/components/structures/SearchBox.js
|
||||||
src/components/structures/TimelinePanel.js
|
src/components/structures/TimelinePanel.js
|
||||||
src/components/structures/UploadBar.js
|
src/components/structures/UploadBar.js
|
||||||
|
src/components/structures/UserSettings.js
|
||||||
src/components/structures/ViewSource.js
|
src/components/structures/ViewSource.js
|
||||||
src/components/views/avatars/BaseAvatar.js
|
src/components/views/avatars/BaseAvatar.js
|
||||||
src/components/views/avatars/GroupAvatar.js
|
|
||||||
src/components/views/avatars/MemberAvatar.js
|
src/components/views/avatars/MemberAvatar.js
|
||||||
src/components/views/create_room/RoomAlias.js
|
src/components/views/create_room/RoomAlias.js
|
||||||
src/components/views/dialogs/BugReportDialog.js
|
|
||||||
src/components/views/dialogs/ChangelogDialog.js
|
src/components/views/dialogs/ChangelogDialog.js
|
||||||
src/components/views/dialogs/ChatCreateOrReuseDialog.js
|
|
||||||
src/components/views/dialogs/DeactivateAccountDialog.js
|
src/components/views/dialogs/DeactivateAccountDialog.js
|
||||||
src/components/views/dialogs/SetPasswordDialog.js
|
src/components/views/dialogs/SetPasswordDialog.js
|
||||||
src/components/views/dialogs/UnknownDeviceDialog.js
|
src/components/views/dialogs/UnknownDeviceDialog.js
|
||||||
|
@ -41,12 +34,12 @@ src/components/views/directory/NetworkDropdown.js
|
||||||
src/components/views/elements/AddressSelector.js
|
src/components/views/elements/AddressSelector.js
|
||||||
src/components/views/elements/DeviceVerifyButtons.js
|
src/components/views/elements/DeviceVerifyButtons.js
|
||||||
src/components/views/elements/DirectorySearchBox.js
|
src/components/views/elements/DirectorySearchBox.js
|
||||||
src/components/views/elements/EditableText.js
|
|
||||||
src/components/views/elements/ImageView.js
|
src/components/views/elements/ImageView.js
|
||||||
src/components/views/elements/InlineSpinner.js
|
src/components/views/elements/InlineSpinner.js
|
||||||
src/components/views/elements/MemberEventListSummary.js
|
src/components/views/elements/MemberEventListSummary.js
|
||||||
src/components/views/elements/Spinner.js
|
src/components/views/elements/Spinner.js
|
||||||
src/components/views/elements/TintableSvg.js
|
src/components/views/elements/TintableSvg.js
|
||||||
|
src/components/views/elements/UserInfo.js
|
||||||
src/components/views/elements/UserSelector.js
|
src/components/views/elements/UserSelector.js
|
||||||
src/components/views/globals/MatrixToolbar.js
|
src/components/views/globals/MatrixToolbar.js
|
||||||
src/components/views/globals/NewVersionBar.js
|
src/components/views/globals/NewVersionBar.js
|
||||||
|
@ -65,7 +58,6 @@ src/components/views/room_settings/UrlPreviewSettings.js
|
||||||
src/components/views/rooms/Autocomplete.js
|
src/components/views/rooms/Autocomplete.js
|
||||||
src/components/views/rooms/AuxPanel.js
|
src/components/views/rooms/AuxPanel.js
|
||||||
src/components/views/rooms/EntityTile.js
|
src/components/views/rooms/EntityTile.js
|
||||||
src/components/views/rooms/EventTile.js
|
|
||||||
src/components/views/rooms/LinkPreviewWidget.js
|
src/components/views/rooms/LinkPreviewWidget.js
|
||||||
src/components/views/rooms/MemberDeviceInfo.js
|
src/components/views/rooms/MemberDeviceInfo.js
|
||||||
src/components/views/rooms/MemberInfo.js
|
src/components/views/rooms/MemberInfo.js
|
||||||
|
@ -73,12 +65,11 @@ src/components/views/rooms/MemberList.js
|
||||||
src/components/views/rooms/MemberTile.js
|
src/components/views/rooms/MemberTile.js
|
||||||
src/components/views/rooms/MessageComposer.js
|
src/components/views/rooms/MessageComposer.js
|
||||||
src/components/views/rooms/MessageComposerInput.js
|
src/components/views/rooms/MessageComposerInput.js
|
||||||
|
src/components/views/rooms/PinnedEventTile.js
|
||||||
src/components/views/rooms/RoomDropTarget.js
|
src/components/views/rooms/RoomDropTarget.js
|
||||||
src/components/views/rooms/RoomList.js
|
src/components/views/rooms/RoomList.js
|
||||||
src/components/views/rooms/RoomPreviewBar.js
|
src/components/views/rooms/RoomPreviewBar.js
|
||||||
src/components/views/rooms/RoomSettings.js
|
src/components/views/rooms/RoomSettings.js
|
||||||
src/components/views/rooms/RoomTile.js
|
|
||||||
src/components/views/rooms/RoomTooltip.js
|
|
||||||
src/components/views/rooms/SearchableEntityList.js
|
src/components/views/rooms/SearchableEntityList.js
|
||||||
src/components/views/rooms/SearchBar.js
|
src/components/views/rooms/SearchBar.js
|
||||||
src/components/views/rooms/SearchResultTile.js
|
src/components/views/rooms/SearchResultTile.js
|
||||||
|
@ -86,12 +77,12 @@ src/components/views/rooms/TopUnreadMessagesBar.js
|
||||||
src/components/views/rooms/UserTile.js
|
src/components/views/rooms/UserTile.js
|
||||||
src/components/views/settings/AddPhoneNumber.js
|
src/components/views/settings/AddPhoneNumber.js
|
||||||
src/components/views/settings/ChangeAvatar.js
|
src/components/views/settings/ChangeAvatar.js
|
||||||
src/components/views/settings/ChangeDisplayName.js
|
|
||||||
src/components/views/settings/ChangePassword.js
|
src/components/views/settings/ChangePassword.js
|
||||||
src/components/views/settings/DevicesPanel.js
|
src/components/views/settings/DevicesPanel.js
|
||||||
src/components/views/settings/IntegrationsManager.js
|
src/components/views/settings/IntegrationsManager.js
|
||||||
src/components/views/settings/Notifications.js
|
src/components/views/settings/Notifications.js
|
||||||
src/ContentMessages.js
|
src/ContentMessages.js
|
||||||
|
src/GroupAddressPicker.js
|
||||||
src/HtmlUtils.js
|
src/HtmlUtils.js
|
||||||
src/ImageUtils.js
|
src/ImageUtils.js
|
||||||
src/languageHandler.js
|
src/languageHandler.js
|
||||||
|
@ -135,6 +126,7 @@ test/components/structures/TimelinePanel-test.js
|
||||||
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
||||||
test/components/views/login/RegistrationForm-test.js
|
test/components/views/login/RegistrationForm-test.js
|
||||||
test/components/views/rooms/MessageComposerInput-test.js
|
test/components/views/rooms/MessageComposerInput-test.js
|
||||||
|
test/components/views/rooms/RoomSettings-test.js
|
||||||
test/mock-clock.js
|
test/mock-clock.js
|
||||||
test/notifications/ContentRules-test.js
|
test/notifications/ContentRules-test.js
|
||||||
test/notifications/PushRuleVectorState-test.js
|
test/notifications/PushRuleVectorState-test.js
|
||||||
|
|
184
CHANGELOG.md
|
@ -1,3 +1,187 @@
|
||||||
|
Changes in [0.12.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.8) (2018-06-29)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.8-rc.2...v0.12.8)
|
||||||
|
|
||||||
|
* Revert "affix copyButton so that it doesn't get scrolled horizontally"
|
||||||
|
[\#2013](https://github.com/matrix-org/matrix-react-sdk/pull/2013)
|
||||||
|
* don't fire share dialog when clicking timestamp of event
|
||||||
|
[\#2017](https://github.com/matrix-org/matrix-react-sdk/pull/2017)
|
||||||
|
* when the user switches room, close room settings
|
||||||
|
[\#2019](https://github.com/matrix-org/matrix-react-sdk/pull/2019)
|
||||||
|
|
||||||
|
Changes in [0.12.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.8-rc.2) (2018-06-22)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.8-rc.1...v0.12.8-rc.2)
|
||||||
|
|
||||||
|
* slash got consumed in the consolidation
|
||||||
|
[\#1998](https://github.com/matrix-org/matrix-react-sdk/pull/1998)
|
||||||
|
|
||||||
|
Changes in [0.12.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.8-rc.1) (2018-06-21)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.7...v0.12.8-rc.1)
|
||||||
|
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1997](https://github.com/matrix-org/matrix-react-sdk/pull/1997)
|
||||||
|
* refactor, consolidate and improve SlashCommands
|
||||||
|
[\#1988](https://github.com/matrix-org/matrix-react-sdk/pull/1988)
|
||||||
|
* Take replies out of labs!
|
||||||
|
[\#1996](https://github.com/matrix-org/matrix-react-sdk/pull/1996)
|
||||||
|
* re-merge reset PR
|
||||||
|
[\#1987](https://github.com/matrix-org/matrix-react-sdk/pull/1987)
|
||||||
|
* once command has a space, strict match instead of fuzzy match
|
||||||
|
[\#1985](https://github.com/matrix-org/matrix-react-sdk/pull/1985)
|
||||||
|
* Fix matrix.to URL RegExp
|
||||||
|
[\#1986](https://github.com/matrix-org/matrix-react-sdk/pull/1986)
|
||||||
|
* Fix blank sticker picker
|
||||||
|
[\#1984](https://github.com/matrix-org/matrix-react-sdk/pull/1984)
|
||||||
|
* fix e2ee file/media stuff
|
||||||
|
[\#1972](https://github.com/matrix-org/matrix-react-sdk/pull/1972)
|
||||||
|
* right click for room tile context menu
|
||||||
|
[\#1978](https://github.com/matrix-org/matrix-react-sdk/pull/1978)
|
||||||
|
* only show m.room.message in FilePanel
|
||||||
|
[\#1983](https://github.com/matrix-org/matrix-react-sdk/pull/1983)
|
||||||
|
* improve command provider
|
||||||
|
[\#1981](https://github.com/matrix-org/matrix-react-sdk/pull/1981)
|
||||||
|
* affix copyButton so that it doesn't get scrolled horizontally
|
||||||
|
[\#1980](https://github.com/matrix-org/matrix-react-sdk/pull/1980)
|
||||||
|
* split continuation if there is a gap in conversation
|
||||||
|
[\#1979](https://github.com/matrix-org/matrix-react-sdk/pull/1979)
|
||||||
|
* fix a bunch of instances of react console spam
|
||||||
|
[\#1973](https://github.com/matrix-org/matrix-react-sdk/pull/1973)
|
||||||
|
* Track decryption success/failure rate with piwik
|
||||||
|
[\#1949](https://github.com/matrix-org/matrix-react-sdk/pull/1949)
|
||||||
|
* route matrix.to/#/+... links internally (not just group ids)
|
||||||
|
[\#1975](https://github.com/matrix-org/matrix-react-sdk/pull/1975)
|
||||||
|
* implement `hitting enter after Ctrl-K should switch to the first result`
|
||||||
|
[\#1976](https://github.com/matrix-org/matrix-react-sdk/pull/1976)
|
||||||
|
* Remove tag panel feature flag
|
||||||
|
[\#1970](https://github.com/matrix-org/matrix-react-sdk/pull/1970)
|
||||||
|
* QuestionDialog pass hasCancelButton to DialogButtons
|
||||||
|
[\#1968](https://github.com/matrix-org/matrix-react-sdk/pull/1968)
|
||||||
|
* check type before msgtype in the case of `m.sticker` with msgtype
|
||||||
|
[\#1965](https://github.com/matrix-org/matrix-react-sdk/pull/1965)
|
||||||
|
* apply roomlist searchFilter to aliases if it begins with a `#`
|
||||||
|
[\#1957](https://github.com/matrix-org/matrix-react-sdk/pull/1957)
|
||||||
|
* Share Dialog
|
||||||
|
[\#1948](https://github.com/matrix-org/matrix-react-sdk/pull/1948)
|
||||||
|
* make RoomTooltip generic and add ContextMenu&Tooltip to GroupInviteTile
|
||||||
|
[\#1950](https://github.com/matrix-org/matrix-react-sdk/pull/1950)
|
||||||
|
* Fix widgets re-appearing after being deleted
|
||||||
|
[\#1958](https://github.com/matrix-org/matrix-react-sdk/pull/1958)
|
||||||
|
* Fix crash on unspecified thumbnail info, and handle gracefully
|
||||||
|
[\#1967](https://github.com/matrix-org/matrix-react-sdk/pull/1967)
|
||||||
|
* fix styling of clearButton when its not there
|
||||||
|
[\#1964](https://github.com/matrix-org/matrix-react-sdk/pull/1964)
|
||||||
|
* Implement slightly magical CSS soln. to thumbnail sizing
|
||||||
|
[\#1912](https://github.com/matrix-org/matrix-react-sdk/pull/1912)
|
||||||
|
* Select audio output for WebRTC
|
||||||
|
[\#1932](https://github.com/matrix-org/matrix-react-sdk/pull/1932)
|
||||||
|
* move css rule to be more generic; remove overriden rule
|
||||||
|
[\#1962](https://github.com/matrix-org/matrix-react-sdk/pull/1962)
|
||||||
|
* improve tag panel accessibility and remove a no-op dispatch
|
||||||
|
[\#1960](https://github.com/matrix-org/matrix-react-sdk/pull/1960)
|
||||||
|
* Revert "Fix exception when opening dev tools"
|
||||||
|
[\#1963](https://github.com/matrix-org/matrix-react-sdk/pull/1963)
|
||||||
|
* fix message appears unencrypted while encrypting and not_sent
|
||||||
|
[\#1959](https://github.com/matrix-org/matrix-react-sdk/pull/1959)
|
||||||
|
* Fix exception when opening dev tools
|
||||||
|
[\#1961](https://github.com/matrix-org/matrix-react-sdk/pull/1961)
|
||||||
|
* show redacted stickers like other redacted messages
|
||||||
|
[\#1956](https://github.com/matrix-org/matrix-react-sdk/pull/1956)
|
||||||
|
* add mx_filterFlipColor to mx_MemberInfo_cancel img
|
||||||
|
[\#1951](https://github.com/matrix-org/matrix-react-sdk/pull/1951)
|
||||||
|
* don't set the displayname on registration as Synapse now does it
|
||||||
|
[\#1953](https://github.com/matrix-org/matrix-react-sdk/pull/1953)
|
||||||
|
* allow CreateRoom to scale properly horizontally
|
||||||
|
[\#1955](https://github.com/matrix-org/matrix-react-sdk/pull/1955)
|
||||||
|
* Keep context menus that extend downwards vertically on screen
|
||||||
|
[\#1952](https://github.com/matrix-org/matrix-react-sdk/pull/1952)
|
||||||
|
* re-run checkIfAlone if a member change occurred in the active room
|
||||||
|
[\#1947](https://github.com/matrix-org/matrix-react-sdk/pull/1947)
|
||||||
|
* Persist pinned message open-ness between room switches
|
||||||
|
[\#1935](https://github.com/matrix-org/matrix-react-sdk/pull/1935)
|
||||||
|
* Pinned message cosmetic improvements
|
||||||
|
[\#1933](https://github.com/matrix-org/matrix-react-sdk/pull/1933)
|
||||||
|
* Update sinon to 5.0.7
|
||||||
|
[\#1916](https://github.com/matrix-org/matrix-react-sdk/pull/1916)
|
||||||
|
* re-run checkIfAlone if a member change occurred in the active room
|
||||||
|
[\#1946](https://github.com/matrix-org/matrix-react-sdk/pull/1946)
|
||||||
|
* Replace "Login as guest" with "Try the app first" on login page
|
||||||
|
[\#1937](https://github.com/matrix-org/matrix-react-sdk/pull/1937)
|
||||||
|
* kill stream when using gUM for permission to device labels to turn off
|
||||||
|
camera
|
||||||
|
[\#1931](https://github.com/matrix-org/matrix-react-sdk/pull/1931)
|
||||||
|
|
||||||
|
Changes in [0.12.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.7) (2018-06-12)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.7-rc.1...v0.12.7)
|
||||||
|
|
||||||
|
* No changes since rc.1
|
||||||
|
|
||||||
|
Changes in [0.12.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.7-rc.1) (2018-06-06)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.6...v0.12.7-rc.1)
|
||||||
|
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1944](https://github.com/matrix-org/matrix-react-sdk/pull/1944)
|
||||||
|
* Import react as React in src/components/views/elements/DNDTagTile.js
|
||||||
|
[\#1943](https://github.com/matrix-org/matrix-react-sdk/pull/1943)
|
||||||
|
* Fix click on faded left/right/middle panel -> close settings
|
||||||
|
[\#1940](https://github.com/matrix-org/matrix-react-sdk/pull/1940)
|
||||||
|
* Add null-guard to support browsers that don't support performance
|
||||||
|
[\#1942](https://github.com/matrix-org/matrix-react-sdk/pull/1942)
|
||||||
|
* Support third party integration managers in AppPermission
|
||||||
|
[\#1455](https://github.com/matrix-org/matrix-react-sdk/pull/1455)
|
||||||
|
* Update pinned messages in real time
|
||||||
|
[\#1934](https://github.com/matrix-org/matrix-react-sdk/pull/1934)
|
||||||
|
* Expose at-room power level setting
|
||||||
|
[\#1938](https://github.com/matrix-org/matrix-react-sdk/pull/1938)
|
||||||
|
|
||||||
|
Changes in [0.12.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.6) (2018-05-25)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.6-rc.1...v0.12.6)
|
||||||
|
|
||||||
|
* No changes since v0.12.6-rc.1
|
||||||
|
|
||||||
|
Changes in [0.12.6-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.6-rc.1) (2018-05-24)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.5...v0.12.6-rc.1)
|
||||||
|
|
||||||
|
* Add a "reload widget" button.
|
||||||
|
[\#1920](https://github.com/matrix-org/matrix-react-sdk/pull/1920)
|
||||||
|
* Make devTools styling more consistent and easier to edit event data.
|
||||||
|
[\#1923](https://github.com/matrix-org/matrix-react-sdk/pull/1923)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1930](https://github.com/matrix-org/matrix-react-sdk/pull/1930)
|
||||||
|
* Cookie bar update
|
||||||
|
[\#1929](https://github.com/matrix-org/matrix-react-sdk/pull/1929)
|
||||||
|
* Message for leaving server notices room
|
||||||
|
[\#1928](https://github.com/matrix-org/matrix-react-sdk/pull/1928)
|
||||||
|
* More thorough check of IM URL validity.
|
||||||
|
[\#1927](https://github.com/matrix-org/matrix-react-sdk/pull/1927)
|
||||||
|
* Add usage data link to cookie bar
|
||||||
|
[\#1926](https://github.com/matrix-org/matrix-react-sdk/pull/1926)
|
||||||
|
* Change wording and appearance of Deactivate Account dialog
|
||||||
|
[\#1925](https://github.com/matrix-org/matrix-react-sdk/pull/1925)
|
||||||
|
* fix membership list ordering when presence is disabled.
|
||||||
|
[\#1924](https://github.com/matrix-org/matrix-react-sdk/pull/1924)
|
||||||
|
* Implement erasure option upon deactivation
|
||||||
|
[\#1922](https://github.com/matrix-org/matrix-react-sdk/pull/1922)
|
||||||
|
* Add cookie warning to widget warning (AppPermission)
|
||||||
|
[\#1921](https://github.com/matrix-org/matrix-react-sdk/pull/1921)
|
||||||
|
* Terms and Conditions dialog
|
||||||
|
[\#1919](https://github.com/matrix-org/matrix-react-sdk/pull/1919)
|
||||||
|
* improve privileged section users in room settings
|
||||||
|
[\#1902](https://github.com/matrix-org/matrix-react-sdk/pull/1902)
|
||||||
|
* Space between sentences in 'leave room' warning
|
||||||
|
[\#1918](https://github.com/matrix-org/matrix-react-sdk/pull/1918)
|
||||||
|
* Specify valid address types to "Start a chat" dialog
|
||||||
|
[\#1908](https://github.com/matrix-org/matrix-react-sdk/pull/1908)
|
||||||
|
* Implement opt-in analytics with cookie bar
|
||||||
|
[\#1906](https://github.com/matrix-org/matrix-react-sdk/pull/1906)
|
||||||
|
* Fix vector-im/riot-web#6523 Emoji rendering destroys paragraphs
|
||||||
|
[\#1910](https://github.com/matrix-org/matrix-react-sdk/pull/1910)
|
||||||
|
|
||||||
Changes in [0.12.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.5) (2018-05-17)
|
Changes in [0.12.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.5) (2018-05-17)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.4...v0.12.5)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.4...v0.12.5)
|
||||||
|
|
339
package-lock.json
generated
|
@ -1,9 +1,18 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.12.2",
|
"version": "0.12.7",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sinonjs/formatio": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"samsam": "1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"accepts": {
|
"accepts": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
|
||||||
|
@ -44,14 +53,14 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ajv": {
|
"ajv": {
|
||||||
"version": "5.2.3",
|
"version": "5.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
|
||||||
"integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=",
|
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"co": "4.6.0",
|
"co": "4.6.0",
|
||||||
"fast-deep-equal": "1.0.0",
|
"fast-deep-equal": "1.1.0",
|
||||||
"json-schema-traverse": "0.3.1",
|
"fast-json-stable-stringify": "2.0.0",
|
||||||
"json-stable-stringify": "1.0.1"
|
"json-schema-traverse": "0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ajv-keywords": {
|
"ajv-keywords": {
|
||||||
|
@ -238,9 +247,9 @@
|
||||||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
||||||
},
|
},
|
||||||
"aws4": {
|
"aws4": {
|
||||||
"version": "1.6.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz",
|
||||||
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
|
"integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w=="
|
||||||
},
|
},
|
||||||
"babel-cli": {
|
"babel-cli": {
|
||||||
"version": "6.26.0",
|
"version": "6.26.0",
|
||||||
|
@ -1200,11 +1209,6 @@
|
||||||
"type-is": "1.6.15"
|
"type-is": "1.6.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"boom": {
|
|
||||||
"version": "4.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
|
|
||||||
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE="
|
|
||||||
},
|
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
|
||||||
|
@ -1440,9 +1444,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"combined-stream": {
|
"combined-stream": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
|
||||||
"integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=",
|
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"delayed-stream": "1.0.0"
|
"delayed-stream": "1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -1580,21 +1584,6 @@
|
||||||
"object-assign": "4.1.1"
|
"object-assign": "4.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cryptiles": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
|
|
||||||
"integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
|
|
||||||
"requires": {
|
|
||||||
"boom": "5.2.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"boom": {
|
|
||||||
"version": "5.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
|
|
||||||
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"crypto-browserify": {
|
"crypto-browserify": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.3.0.tgz",
|
||||||
|
@ -1713,6 +1702,12 @@
|
||||||
"integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
|
"integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"diff": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"doctrine": {
|
"doctrine": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz",
|
||||||
|
@ -2393,9 +2388,14 @@
|
||||||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
|
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
|
||||||
},
|
},
|
||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
|
||||||
"integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8="
|
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
|
||||||
|
},
|
||||||
|
"fast-json-stable-stringify": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
|
||||||
},
|
},
|
||||||
"fast-levenshtein": {
|
"fast-levenshtein": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
|
@ -2616,24 +2616,15 @@
|
||||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
||||||
},
|
},
|
||||||
"form-data": {
|
"form-data": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
|
||||||
"integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=",
|
"integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"asynckit": "0.4.0",
|
"asynckit": "0.4.0",
|
||||||
"combined-stream": "1.0.5",
|
"combined-stream": "1.0.6",
|
||||||
"mime-types": "2.1.17"
|
"mime-types": "2.1.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"formatio": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz",
|
|
||||||
"integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"samsam": "1.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fs-access": {
|
"fs-access": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
|
||||||
|
@ -3679,7 +3670,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
|
||||||
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
|
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"ajv": "5.2.3",
|
"ajv": "5.5.2",
|
||||||
"har-schema": "2.0.0"
|
"har-schema": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3730,16 +3721,6 @@
|
||||||
"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
|
"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"hawk": {
|
|
||||||
"version": "6.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
|
|
||||||
"integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
|
|
||||||
"requires": {
|
|
||||||
"boom": "4.3.1",
|
|
||||||
"cryptiles": "3.1.2",
|
|
||||||
"sntp": "2.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"he": {
|
"he": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
|
||||||
|
@ -3808,7 +3789,7 @@
|
||||||
"requires": {
|
"requires": {
|
||||||
"assert-plus": "1.0.0",
|
"assert-plus": "1.0.0",
|
||||||
"jsprim": "1.4.1",
|
"jsprim": "1.4.1",
|
||||||
"sshpk": "1.13.1"
|
"sshpk": "1.14.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"https-browserify": {
|
"https-browserify": {
|
||||||
|
@ -4221,6 +4202,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
|
||||||
"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
|
"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"jsonify": "0.0.0"
|
"jsonify": "0.0.0"
|
||||||
}
|
}
|
||||||
|
@ -4245,7 +4227,8 @@
|
||||||
"jsonify": {
|
"jsonify": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
|
||||||
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
|
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"jsonpointer": {
|
"jsonpointer": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
|
@ -4273,6 +4256,12 @@
|
||||||
"array-includes": "3.0.3"
|
"array-includes": "3.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"just-extend": {
|
||||||
|
"version": "1.1.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz",
|
||||||
|
"integrity": "sha512-mJVp13Ix6gFo3SBAy9U/kL+oeZqzlYYYLQBwXVBlVzIsZwBqGREnOro24oC/8s8aox+rJhtZ2DiQof++IrkA+g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"karma": {
|
"karma": {
|
||||||
"version": "1.7.1",
|
"version": "1.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz",
|
||||||
|
@ -4454,13 +4443,45 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"linkifyjs": {
|
"linkifyjs": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-2.1.6.tgz",
|
||||||
"integrity": "sha512-8FqxPXQDLjI2nNHlM7eGewxE6DHvMbtiW0AiXzm0s4RkTwVZYRDTeVXkiRxLHTd4CuRBQY/JPtvtqJWdS7gHyA==",
|
"integrity": "sha512-nA94bEM9rmt7Iu4OEIYSKpW+Dy6fhlBTjk2Bg9bFuxHQYcy+lWq2EleHb0rp/ev8oBO82vLHZctM5YlSR5DTzw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"jquery": "3.2.1",
|
"jquery": "3.3.1",
|
||||||
"react": "15.6.2",
|
"react": "16.4.1",
|
||||||
"react-dom": "15.6.2"
|
"react-dom": "16.4.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jquery": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"version": "16.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-16.4.1.tgz",
|
||||||
|
"integrity": "sha512-3GEs0giKp6E0Oh/Y9ZC60CmYgUPnp7voH9fbjWsvXtYFb4EWtgQub0ADSq0sJR0BbHc4FThLLtzlcFaFXIorwg==",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"fbjs": "0.8.16",
|
||||||
|
"loose-envify": "1.3.1",
|
||||||
|
"object-assign": "4.1.1",
|
||||||
|
"prop-types": "15.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"version": "16.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.1.tgz",
|
||||||
|
"integrity": "sha512-1Gin+wghF/7gl4Cqcvr1DxFX2Osz7ugxSwl6gBqCMpdrxHjIFUS7GYxrFftZ9Ln44FHw0JxCFD9YtZsrbR5/4A==",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"fbjs": "0.8.16",
|
||||||
|
"loose-envify": "1.3.1",
|
||||||
|
"object-assign": "4.1.1",
|
||||||
|
"prop-types": "15.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"loader-utils": {
|
"loader-utils": {
|
||||||
|
@ -4491,6 +4512,12 @@
|
||||||
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
|
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"lodash.get": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
|
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"lodash.pickby": {
|
"lodash.pickby": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
|
||||||
|
@ -4534,10 +4561,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lolex": {
|
"lolex": {
|
||||||
"version": "1.3.2",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.0.tgz",
|
||||||
"integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=",
|
"integrity": "sha512-uJkH2e0BVfU5KOJUevbTOtpDduooSarH5PopO+LfM/vZf8Z9sJzODqKev804JYM2i++ktJfUmC1le4LwFQ1VMg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"longest": {
|
"longest": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
@ -4560,16 +4586,16 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"matrix-js-sdk": {
|
"matrix-js-sdk": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.10.4.tgz",
|
||||||
"integrity": "sha512-BLo+Okn2o///TyWBKtjFXvhlD32vGfr10eTE51hHx/jwaXO82VyGMzMi+IDPS4SDYUbvXI7PpamECeh9TXnV2w==",
|
"integrity": "sha512-jmO08eml0mr+us2Xs9F9UD2U6gX/MVD20QEqrEt3p+cuZ043OEWCg6Ko8mR65P/JteqjXMz+TXOMmfcxLwCLFA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"another-json": "0.2.0",
|
"another-json": "0.2.0",
|
||||||
"babel-runtime": "6.26.0",
|
"babel-runtime": "6.26.0",
|
||||||
"bluebird": "3.5.1",
|
"bluebird": "3.5.1",
|
||||||
"browser-request": "0.3.3",
|
"browser-request": "0.3.3",
|
||||||
"content-type": "1.0.4",
|
"content-type": "1.0.4",
|
||||||
"request": "2.83.0"
|
"request": "2.87.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"matrix-mock-request": {
|
"matrix-mock-request": {
|
||||||
|
@ -4790,6 +4816,27 @@
|
||||||
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
|
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"nise": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/nise/-/nise-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-v1J/FLUB9PfGqZLGDBhQqODkbLotP0WtLo9R4EJY2PPu5f5Xg4o0rA8FDlmrjFSv9vBBKcfnOSpfYYuu5RTHqg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@sinonjs/formatio": "2.0.0",
|
||||||
|
"just-extend": "1.1.27",
|
||||||
|
"lolex": "2.6.0",
|
||||||
|
"path-to-regexp": "1.7.0",
|
||||||
|
"text-encoding": "0.6.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lolex": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-e1UtIo1pbrIqEXib/yMjHciyqkng5lc0rrIbytgjmRgDR9+2ceNIAcwOWSgylRjoEP9VdVguCSRwnNmlbnOUwA==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node-fetch": {
|
"node-fetch": {
|
||||||
"version": "1.7.3",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
|
||||||
|
@ -5093,6 +5140,23 @@
|
||||||
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
|
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"path-to-regexp": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
|
||||||
|
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"isarray": "0.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"isarray": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"pbkdf2-compat": {
|
"pbkdf2-compat": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz",
|
||||||
|
@ -5215,6 +5279,19 @@
|
||||||
"integrity": "sha1-ZZ3p8s+NzCehSBJ28gU3cnI4LnM=",
|
"integrity": "sha1-ZZ3p8s+NzCehSBJ28gU3cnI4LnM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"qr.js": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
|
||||||
|
"integrity": "sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8="
|
||||||
|
},
|
||||||
|
"qrcode-react": {
|
||||||
|
"version": "0.1.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode-react/-/qrcode-react-0.1.16.tgz",
|
||||||
|
"integrity": "sha512-FK+QCfFqCQMSxUE1byzglERJQkwKqXYvYMCS+/Ad2zACJOfoHkHHtRqsQQPji7lfb1y1qCXLvL+3eP1hAfg8Ng==",
|
||||||
|
"requires": {
|
||||||
|
"qr.js": "0.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.5.1",
|
"version": "6.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
|
||||||
|
@ -5557,19 +5634,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"version": "2.83.0",
|
"version": "2.87.0",
|
||||||
"resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz",
|
"resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz",
|
||||||
"integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==",
|
"integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"aws-sign2": "0.7.0",
|
"aws-sign2": "0.7.0",
|
||||||
"aws4": "1.6.0",
|
"aws4": "1.7.0",
|
||||||
"caseless": "0.12.0",
|
"caseless": "0.12.0",
|
||||||
"combined-stream": "1.0.5",
|
"combined-stream": "1.0.6",
|
||||||
"extend": "3.0.1",
|
"extend": "3.0.1",
|
||||||
"forever-agent": "0.6.1",
|
"forever-agent": "0.6.1",
|
||||||
"form-data": "2.3.1",
|
"form-data": "2.3.2",
|
||||||
"har-validator": "5.0.3",
|
"har-validator": "5.0.3",
|
||||||
"hawk": "6.0.2",
|
|
||||||
"http-signature": "1.2.0",
|
"http-signature": "1.2.0",
|
||||||
"is-typedarray": "1.0.0",
|
"is-typedarray": "1.0.0",
|
||||||
"isstream": "0.1.2",
|
"isstream": "0.1.2",
|
||||||
|
@ -5579,10 +5655,9 @@
|
||||||
"performance-now": "2.1.0",
|
"performance-now": "2.1.0",
|
||||||
"qs": "6.5.1",
|
"qs": "6.5.1",
|
||||||
"safe-buffer": "5.1.1",
|
"safe-buffer": "5.1.1",
|
||||||
"stringstream": "0.0.5",
|
"tough-cookie": "2.3.4",
|
||||||
"tough-cookie": "2.3.3",
|
|
||||||
"tunnel-agent": "0.6.0",
|
"tunnel-agent": "0.6.0",
|
||||||
"uuid": "3.1.0"
|
"uuid": "3.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-json": {
|
"require-json": {
|
||||||
|
@ -5697,10 +5772,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
||||||
},
|
},
|
||||||
|
"safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
|
},
|
||||||
"samsam": {
|
"samsam": {
|
||||||
"version": "1.1.2",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz",
|
||||||
"integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=",
|
"integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"sanitize-html": {
|
"sanitize-html": {
|
||||||
|
@ -5770,15 +5850,41 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sinon": {
|
"sinon": {
|
||||||
"version": "1.17.7",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz",
|
"resolved": "https://registry.npmjs.org/sinon/-/sinon-5.0.7.tgz",
|
||||||
"integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=",
|
"integrity": "sha512-GvNLrwpvLZ8jIMZBUhHGUZDq5wlUdceJWyHvZDmqBxnjazpxY1L0FNbGBX6VpcOEoQ8Q4XMWFzm2myJMvx+VjA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"formatio": "1.1.1",
|
"@sinonjs/formatio": "2.0.0",
|
||||||
"lolex": "1.3.2",
|
"diff": "3.5.0",
|
||||||
"samsam": "1.1.2",
|
"lodash.get": "4.4.2",
|
||||||
"util": "0.10.3"
|
"lolex": "2.6.0",
|
||||||
|
"nise": "1.3.3",
|
||||||
|
"supports-color": "5.4.0",
|
||||||
|
"type-detect": "4.0.8"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||||
|
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"lolex": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-e1UtIo1pbrIqEXib/yMjHciyqkng5lc0rrIbytgjmRgDR9+2ceNIAcwOWSgylRjoEP9VdVguCSRwnNmlbnOUwA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"supports-color": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"has-flag": "3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"slash": {
|
"slash": {
|
||||||
|
@ -5793,11 +5899,6 @@
|
||||||
"integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
|
"integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"sntp": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz",
|
|
||||||
"integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys="
|
|
||||||
},
|
|
||||||
"socket.io": {
|
"socket.io": {
|
||||||
"version": "1.7.3",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz",
|
||||||
|
@ -5995,9 +6096,9 @@
|
||||||
"integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw="
|
"integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw="
|
||||||
},
|
},
|
||||||
"sshpk": {
|
"sshpk": {
|
||||||
"version": "1.13.1",
|
"version": "1.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
|
||||||
"integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
|
"integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"asn1": "0.2.3",
|
"asn1": "0.2.3",
|
||||||
"assert-plus": "1.0.0",
|
"assert-plus": "1.0.0",
|
||||||
|
@ -6006,6 +6107,7 @@
|
||||||
"ecc-jsbn": "0.1.1",
|
"ecc-jsbn": "0.1.1",
|
||||||
"getpass": "0.1.7",
|
"getpass": "0.1.7",
|
||||||
"jsbn": "0.1.1",
|
"jsbn": "0.1.1",
|
||||||
|
"safer-buffer": "2.1.2",
|
||||||
"tweetnacl": "0.14.5"
|
"tweetnacl": "0.14.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6062,11 +6164,6 @@
|
||||||
"safe-buffer": "5.1.1"
|
"safe-buffer": "5.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stringstream": {
|
|
||||||
"version": "0.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
|
|
||||||
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
|
|
||||||
},
|
|
||||||
"strip-ansi": {
|
"strip-ansi": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||||
|
@ -6167,6 +6264,12 @@
|
||||||
"integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=",
|
"integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"text-encoding": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
|
||||||
|
"integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"text-encoding-utf-8": {
|
"text-encoding-utf-8": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.1.tgz",
|
||||||
|
@ -6233,9 +6336,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"tough-cookie": {
|
"tough-cookie": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
|
||||||
"integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
|
"integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"punycode": "1.4.1"
|
"punycode": "1.4.1"
|
||||||
}
|
}
|
||||||
|
@ -6281,6 +6384,12 @@
|
||||||
"prelude-ls": "1.1.2"
|
"prelude-ls": "1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"type-detect": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"type-is": {
|
"type-is": {
|
||||||
"version": "1.6.15",
|
"version": "1.6.15",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz",
|
||||||
|
@ -6401,9 +6510,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"version": "3.1.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.0.tgz",
|
||||||
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
|
"integrity": "sha512-ijO9N2xY/YaOqQ5yz5c4sy2ZjWmA6AR6zASb/gdpeKZ8+948CxwfMW9RrKVk5may6ev8c0/Xguu32e2Llelpqw=="
|
||||||
},
|
},
|
||||||
"v8flags": {
|
"v8flags": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.12.5",
|
"version": "0.12.8",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -70,13 +70,14 @@
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.14",
|
||||||
"highlight.js": "^9.0.0",
|
"highlight.js": "^9.0.0",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.6",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"lolex": "2.3.2",
|
"lolex": "2.3.2",
|
||||||
"matrix-js-sdk": "0.10.2",
|
"matrix-js-sdk": "0.10.5",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"pako": "^1.0.5",
|
"pako": "^1.0.5",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
|
"qrcode-react": "^0.1.16",
|
||||||
"querystring": "^0.2.0",
|
"querystring": "^0.2.0",
|
||||||
"react": "^15.6.0",
|
"react": "^15.6.0",
|
||||||
"react-addons-css-transition-group": "15.3.2",
|
"react-addons-css-transition-group": "15.3.2",
|
||||||
|
@ -135,7 +136,7 @@
|
||||||
"react-addons-test-utils": "^15.4.0",
|
"react-addons-test-utils": "^15.4.0",
|
||||||
"require-json": "0.0.1",
|
"require-json": "0.0.1",
|
||||||
"rimraf": "^2.4.3",
|
"rimraf": "^2.4.3",
|
||||||
"sinon": "^1.17.3",
|
"sinon": "^5.0.7",
|
||||||
"source-map-loader": "^0.2.3",
|
"source-map-loader": "^0.2.3",
|
||||||
"walk": "^2.3.9",
|
"walk": "^2.3.9",
|
||||||
"webpack": "^1.12.14"
|
"webpack": "^1.12.14"
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
||||||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||||
@import "./views/dialogs/_CreateRoomDialog.scss";
|
@import "./views/dialogs/_CreateRoomDialog.scss";
|
||||||
|
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||||
@import "./views/dialogs/_EncryptedEventDialog.scss";
|
@import "./views/dialogs/_EncryptedEventDialog.scss";
|
||||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
@import "./views/dialogs/_SetEmailDialog.scss";
|
@import "./views/dialogs/_SetEmailDialog.scss";
|
||||||
@import "./views/dialogs/_SetMxIdDialog.scss";
|
@import "./views/dialogs/_SetMxIdDialog.scss";
|
||||||
@import "./views/dialogs/_SetPasswordDialog.scss";
|
@import "./views/dialogs/_SetPasswordDialog.scss";
|
||||||
|
@import "./views/dialogs/_ShareDialog.scss";
|
||||||
@import "./views/dialogs/_UnknownDeviceDialog.scss";
|
@import "./views/dialogs/_UnknownDeviceDialog.scss";
|
||||||
@import "./views/directory/_NetworkDropdown.scss";
|
@import "./views/directory/_NetworkDropdown.scss";
|
||||||
@import "./views/elements/_AccessibleButton.scss";
|
@import "./views/elements/_AccessibleButton.scss";
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_ContextualMenu_wrapper {
|
.mx_ContextualMenu_wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 2000;
|
z-index: 5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_background {
|
.mx_ContextualMenu_background {
|
||||||
|
@ -26,7 +26,7 @@ limitations under the License.
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
opacity: 1.0;
|
opacity: 1.0;
|
||||||
z-index: 2000;
|
z-index: 5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu {
|
.mx_ContextualMenu {
|
||||||
|
@ -37,7 +37,7 @@ limitations under the License.
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
z-index: 2001;
|
z-index: 5001;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu.mx_ContextualMenu_right {
|
.mx_ContextualMenu.mx_ContextualMenu_right {
|
||||||
|
|
|
@ -113,6 +113,8 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomStatusBar_connectionLostBar {
|
.mx_RoomStatusBar_connectionLostBar {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
margin-top: 19px;
|
margin-top: 19px;
|
||||||
min-height: 58px;
|
min-height: 58px;
|
||||||
}
|
}
|
||||||
|
@ -132,6 +134,7 @@ limitations under the License.
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomStatusBar_resend_link {
|
.mx_RoomStatusBar_resend_link {
|
||||||
|
|
|
@ -91,6 +91,10 @@ limitations under the License.
|
||||||
background-color: $accent-color;
|
background-color: $accent-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomSubList_label .mx_RoomSubList_badge:hover {
|
||||||
|
filter: brightness($focus-brightness);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
.collapsed .mx_RoomSubList_badge {
|
.collapsed .mx_RoomSubList_badge {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
.mx_TagPanel {
|
.mx_TagPanel {
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
background-color: $tertiary-accent-color;
|
background-color: $tertiary-accent-color;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -25,7 +24,11 @@ limitations under the License.
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagPanel_clearButton {
|
.mx_TagPanel_items_selected {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TagPanel .mx_TagPanel_clearButton_container {
|
||||||
/* Constant height within flex mx_TagPanel */
|
/* Constant height within flex mx_TagPanel */
|
||||||
height: 70px;
|
height: 70px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
|
|
|
@ -23,6 +23,10 @@ limitations under the License.
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CreateRoomDialog_input_container {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CreateRoomDialog_input {
|
.mx_CreateRoomDialog_input {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -30,4 +34,5 @@ limitations under the License.
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
23
res/css/views/dialogs/_DeactivateAccountDialog.scss
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_DeactivateAccountDialog .mx_Dialog_content {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
|
@ -14,8 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_DevTools_content {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_RoomStateExplorer_query {
|
.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_RoomStateExplorer_query {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DevTools_label_left {
|
.mx_DevTools_label_left {
|
||||||
|
@ -38,7 +43,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_DevTools_inputLabelCell
|
.mx_DevTools_inputLabelCell
|
||||||
{
|
{
|
||||||
padding-bottom: 21px;
|
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding-right: 24px;
|
padding-right: 24px;
|
||||||
|
@ -46,7 +50,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_DevTools_inputCell {
|
.mx_DevTools_inputCell {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
padding-bottom: 21px;
|
|
||||||
width: 240px;
|
width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +65,14 @@ limitations under the License.
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_DevTools_textarea {
|
||||||
|
font-size: 12px;
|
||||||
|
max-width: 624px;
|
||||||
|
min-height: 250px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_DevTools_tgl {
|
.mx_DevTools_tgl {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
|
89
res/css/views/dialogs/_ShareDialog.scss
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_ShareDialog {
|
||||||
|
// this is to center the content
|
||||||
|
padding-right: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ShareDialog hr {
|
||||||
|
margin-top: 25px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-color: $light-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ShareDialog_content {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ShareDialog_matrixto {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: solid 1px $light-fg-color;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ShareDialog_matrixto a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ShareDialog_matrixto_link {
|
||||||
|
flex-shrink: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ShareDialog_matrixto_copy {
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 20px;
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
.mx_ShareDialog_matrixto_copy > div {
|
||||||
|
background-image: url($copy-button-url);
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ShareDialog_split {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ShareDialog_qrcode_container {
|
||||||
|
float: left;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 5px; // makes qr code more readable in dark theme
|
||||||
|
border-radius: 5px;
|
||||||
|
height: 256px;
|
||||||
|
width: 256px;
|
||||||
|
margin-right: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ShareDialog_social_container {
|
||||||
|
display: inline-block;
|
||||||
|
width: 299px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ShareDialog_social_icon {
|
||||||
|
display: inline-grid;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
.mx_UserPill,
|
.mx_UserPill,
|
||||||
.mx_RoomPill,
|
.mx_RoomPill,
|
||||||
|
.mx_GroupPill,
|
||||||
.mx_AtRoomPill {
|
.mx_AtRoomPill {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -13,7 +14,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_body .mx_UserPill,
|
.mx_EventTile_body .mx_UserPill,
|
||||||
.mx_EventTile_body .mx_RoomPill {
|
.mx_EventTile_body .mx_RoomPill,
|
||||||
|
.mx_EventTile_body .mx_GroupPill {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,14 +41,25 @@
|
||||||
|
|
||||||
/* More specific to override `.markdown-body a` color */
|
/* More specific to override `.markdown-body a` color */
|
||||||
.mx_EventTile_content .markdown-body a.mx_RoomPill,
|
.mx_EventTile_content .markdown-body a.mx_RoomPill,
|
||||||
.mx_RoomPill {
|
.mx_EventTile_content .markdown-body a.mx_GroupPill,
|
||||||
|
.mx_RoomPill,
|
||||||
|
.mx_GroupPill {
|
||||||
color: $accent-fg-color;
|
color: $accent-fg-color;
|
||||||
background-color: $rte-room-pill-color;
|
background-color: $rte-room-pill-color;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* More specific to override `.markdown-body a` color */
|
||||||
|
.mx_EventTile_content .markdown-body a.mx_GroupPill,
|
||||||
|
.mx_GroupPill {
|
||||||
|
color: $accent-fg-color;
|
||||||
|
background-color: $rte-group-pill-color;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_UserPill .mx_BaseAvatar,
|
.mx_UserPill .mx_BaseAvatar,
|
||||||
.mx_RoomPill .mx_BaseAvatar,
|
.mx_RoomPill .mx_BaseAvatar,
|
||||||
|
.mx_GroupPill .mx_BaseAvatar,
|
||||||
.mx_AtRoomPill .mx_BaseAvatar {
|
.mx_AtRoomPill .mx_BaseAvatar {
|
||||||
position: relative;
|
position: relative;
|
||||||
left: -3px;
|
left: -3px;
|
||||||
|
|
|
@ -20,5 +20,29 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail {
|
.mx_MImageBody_thumbnail {
|
||||||
max-width: 100%;
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MImageBody_thumbnail_container {
|
||||||
|
// Prevent the padding-bottom (added inline in MImageBody.js) from
|
||||||
|
// affecting elements below the container.
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// Make sure the _thumbnail is positioned relative to the _container
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MImageBody_thumbnail_spinner {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner img and TintableSvg should be centered around 0, 0
|
||||||
|
.mx_MImageBody_thumbnail_spinner > * {
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
|
@ -14,33 +14,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_MStickerBody {
|
.mx_MStickerBody_wrapper {
|
||||||
display: block;
|
padding: 20px 0px;
|
||||||
margin-right: 34px;
|
|
||||||
min-height: 110px;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MStickerBody_image_container {
|
.mx_MStickerBody_tooltip {
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MStickerBody_image {
|
|
||||||
max-width: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MStickerBody_image_visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MStickerBody_placeholder {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 1;
|
top: 50%;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MStickerBody_placeholder_invisible {
|
|
||||||
transition: 500ms;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,3 @@ limitations under the License.
|
||||||
.mx_MTextBody {
|
.mx_MTextBody {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MTextBody pre{
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 30vh;
|
|
||||||
}
|
|
||||||
|
|
|
@ -391,6 +391,7 @@ limitations under the License.
|
||||||
.mx_EventTile_content .markdown-body pre {
|
.mx_EventTile_content .markdown-body pre {
|
||||||
overflow-x: overlay;
|
overflow-x: overlay;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
|
max-height: 30vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_content .markdown-body code {
|
.mx_EventTile_content .markdown-body code {
|
||||||
|
@ -399,6 +400,12 @@ limitations under the License.
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_pre_container {
|
||||||
|
// For correct positioning of _copyButton (See TextualBody)
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserted adjacent to <pre> blocks, (See TextualBody)
|
||||||
.mx_EventTile_copyButton {
|
.mx_EventTile_copyButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -412,7 +419,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_body pre {
|
.mx_EventTile_body pre {
|
||||||
position: relative;
|
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,7 +427,7 @@ limitations under the License.
|
||||||
border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter
|
border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_body pre:hover .mx_EventTile_copyButton
|
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton
|
||||||
{
|
{
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ limitations under the License.
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_input {
|
.mx_MessageComposer_input {
|
||||||
|
|
|
@ -25,26 +25,29 @@ limitations under the License.
|
||||||
background-color: $event-selected-color;
|
background-color: $event-selected-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_sender {
|
.mx_PinnedEventTile .mx_PinnedEventTile_sender,
|
||||||
|
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
||||||
color: #868686;
|
color: #868686;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
display: block;
|
display: inline-block;
|
||||||
padding-bottom: 3px;
|
padding-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_EventTile_content {
|
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
||||||
margin-left: 50px;
|
padding-left: 15px;
|
||||||
position: relative;
|
display: none;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_BaseAvatar {
|
.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar {
|
||||||
float: left;
|
float: left;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
|
.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -63,5 +66,12 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_PinnedEventTile_gotoButton {
|
.mx_PinnedEventTile_gotoButton {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.8em;
|
font-size: 0.7em; // Smaller text to avoid conflicting with the layout
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_PinnedEventTile_message {
|
||||||
|
margin-left: 50px;
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
}
|
}
|
12
res/img/button-refresh.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="612px" height="612px" viewBox="0 90 612 612" enable-background="new 0 90 612 612" xml:space="preserve">
|
||||||
|
<path stroke="#76CFA6" fill="#76CFA6" stroke-width="40" stroke-miterlimit="10" d="M517.593,435.2c-9.204,0-17.093,7.053-17.811,16.257
|
||||||
|
c-8.247,99.33-91.8,176.786-193.401,176.786c-106.98,0-194.119-86.54-194.119-192.923c0-104.71,84.389-190.294,189.098-192.924
|
||||||
|
c2.75-0.12,4.901,2.032,4.901,4.781v60.124c0,15.061,16.614,24.146,29.404,16.137l114.989-80.444
|
||||||
|
c11.953-7.53,11.953-24.862,0-32.393l-114.869-79.369c-12.79-8.009-29.405,1.076-29.405,16.137v54.626
|
||||||
|
c0,2.629-2.032,4.781-4.661,4.781C176.929,209.286,76.522,310.649,76.522,435.32c0,126.225,102.917,228.424,229.858,228.424
|
||||||
|
c120.487,0,219.221-91.681,229.022-209.299C536.359,444.046,527.992,435.2,517.593,435.2L517.593,435.2z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
12
res/img/e2e-encrypting.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="10px" height="12px" viewBox="0 0 10 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: sketchtool 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>48BF5D32-306C-4B20-88EB-24B1F743CAC9</title>
|
||||||
|
<desc>Created with sketchtool.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Typing-Indicator" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.4">
|
||||||
|
<g id="typing-indicator" transform="translate(-301.000000, -172.000000)" fill="#76CFA6">
|
||||||
|
<path d="M309.666667,175.666667 C309.666667,173.633333 308.033333,172 306,172 C303.966667,172 302.333333,173.633333 302.333333,175.666667 L302.333333,176.666667 L301,176.666667 L301,184 L306,184 L311,184 L311,176.666667 L309.666667,176.666667 L309.666667,175.666667 Z M306,176.666667 L303.666667,176.666667 L303.666667,175.666667 C303.666667,174.366667 304.7,173.333333 306,173.333333 C307.3,173.333333 308.333333,174.366667 308.333333,175.666667 L308.333333,176.666667 L306,176.666667 L306,176.666667 Z" id="verified_icon"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
12
res/img/e2e-not_sent.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="10px" height="12px" viewBox="0 0 10 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: sketchtool 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>48BF5D32-306C-4B20-88EB-24B1F743CAC9</title>
|
||||||
|
<desc>Created with sketchtool.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Typing-Indicator" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="typing-indicator" transform="translate(-301.000000, -172.000000)" fill="#f44">
|
||||||
|
<path d="M309.666667,175.666667 C309.666667,173.633333 308.033333,172 306,172 C303.966667,172 302.333333,173.633333 302.333333,175.666667 L302.333333,176.666667 L301,176.666667 L301,184 L306,184 L311,184 L311,176.666667 L309.666667,176.666667 L309.666667,175.666667 Z M306,176.666667 L303.666667,176.666667 L303.666667,175.666667 C303.666667,174.366667 304.7,173.333333 306,173.333333 C307.3,173.333333 308.333333,174.366667 308.333333,175.666667 L308.333333,176.666667 L306,176.666667 L306,176.666667 Z" id="verified_icon"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
6
res/img/icons-share.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 481.6 481.6" style="enable-background:new 0 0 481.6 481.6;" xml:space="preserve" width="16px" height="16px">
|
||||||
|
<g>
|
||||||
|
<path stroke="#76CFA6" stroke-width="5" d="M381.6,309.4c-27.7,0-52.4,13.2-68.2,33.6l-132.3-73.9c3.1-8.9,4.8-18.5,4.8-28.4c0-10-1.7-19.5-4.9-28.5l132.2-73.8 c15.7,20.5,40.5,33.8,68.3,33.8c47.4,0,86.1-38.6,86.1-86.1S429,0,381.5,0s-86.1,38.6-86.1,86.1c0,10,1.7,19.6,4.9,28.5 l-132.1,73.8c-15.7-20.6-40.5-33.8-68.3-33.8c-47.4,0-86.1,38.6-86.1,86.1s38.7,86.1,86.2,86.1c27.8,0,52.6-13.3,68.4-33.9 l132.2,73.9c-3.2,9-5,18.7-5,28.7c0,47.4,38.6,86.1,86.1,86.1s86.1-38.6,86.1-86.1S429.1,309.4,381.6,309.4z M381.6,27.1 c32.6,0,59.1,26.5,59.1,59.1s-26.5,59.1-59.1,59.1s-59.1-26.5-59.1-59.1S349.1,27.1,381.6,27.1z M100,299.8 c-32.6,0-59.1-26.5-59.1-59.1s26.5-59.1,59.1-59.1s59.1,26.5,59.1,59.1S132.5,299.8,100,299.8z M381.6,454.5 c-32.6,0-59.1-26.5-59.1-59.1c0-32.6,26.5-59.1,59.1-59.1s59.1,26.5,59.1,59.1C440.7,428,414.2,454.5,381.6,454.5z" fill="#76cfa6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
15
res/img/matrix-m.svg
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 520 520" style="enable-background:new 0 0 520 520;" xml:space="preserve">
|
||||||
|
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||||
|
<path d="M13.7,11.9v496.2h35.7V520H0V0h49.4v11.9H13.7z"/>
|
||||||
|
<path d="M166.3,169.2v25.1h0.7c6.7-9.6,14.8-17,24.2-22.2c9.4-5.3,20.3-7.9,32.5-7.9c11.7,0,22.4,2.3,32.1,6.8
|
||||||
|
c9.7,4.5,17,12.6,22.1,24c5.5-8.1,13-15.3,22.4-21.5c9.4-6.2,20.6-9.3,33.5-9.3c9.8,0,18.9,1.2,27.3,3.6c8.4,2.4,15.5,6.2,21.5,11.5
|
||||||
|
c6,5.3,10.6,12.1,14,20.6c3.3,8.5,5,18.7,5,30.7v124.1h-50.9V249.6c0-6.2-0.2-12.1-0.7-17.6c-0.5-5.5-1.8-10.3-3.9-14.3
|
||||||
|
c-2.2-4.1-5.3-7.3-9.5-9.7c-4.2-2.4-9.9-3.6-17-3.6c-7.2,0-13,1.4-17.4,4.1c-4.4,2.8-7.9,6.3-10.4,10.8c-2.5,4.4-4.2,9.4-5,15.1
|
||||||
|
c-0.8,5.6-1.3,11.3-1.3,17v103.3h-50.9v-104c0-5.5-0.1-10.9-0.4-16.3c-0.2-5.4-1.3-10.3-3.1-14.9c-1.8-4.5-4.8-8.2-9-10.9
|
||||||
|
c-4.2-2.7-10.3-4.1-18.5-4.1c-2.4,0-5.6,0.5-9.5,1.6c-3.9,1.1-7.8,3.1-11.5,6.1c-3.7,3-6.9,7.3-9.5,12.9c-2.6,5.6-3.9,13-3.9,22.1
|
||||||
|
v107.6h-50.9V169.2H166.3z"/>
|
||||||
|
<path d="M506.3,508.1V11.9h-35.7V0H520v520h-49.4v-11.9H506.3z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
BIN
res/img/social/email-1.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
res/img/social/facebook.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
res/img/social/linkedin.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
res/img/social/reddit.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
res/img/social/twitter-2.png
Normal file
After Width: | Height: | Size: 2 KiB |
|
@ -97,6 +97,7 @@ $voip-accept-color: #80f480;
|
||||||
$rte-bg-color: #e9e9e9;
|
$rte-bg-color: #e9e9e9;
|
||||||
$rte-code-bg-color: rgba(0, 0, 0, 0.04);
|
$rte-code-bg-color: rgba(0, 0, 0, 0.04);
|
||||||
$rte-room-pill-color: #aaa;
|
$rte-room-pill-color: #aaa;
|
||||||
|
$rte-group-pill-color: #aaa;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
|
|
@ -39,9 +39,17 @@ function getRedactedHash(hash) {
|
||||||
return hash.replace(hashRegex, "#/$1");
|
return hash.replace(hashRegex, "#/$1");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the current origin and hash separated with a `/`. This does not include query parameters.
|
// Return the current origin, path and hash separated with a `/`. This does
|
||||||
|
// not include query parameters.
|
||||||
function getRedactedUrl() {
|
function getRedactedUrl() {
|
||||||
const { origin, pathname, hash } = window.location;
|
const { origin, hash } = window.location;
|
||||||
|
let { pathname } = window.location;
|
||||||
|
|
||||||
|
// Redact paths which could contain unexpected PII
|
||||||
|
if (origin.startsWith('file://')) {
|
||||||
|
pathname = "/<redacted>/";
|
||||||
|
}
|
||||||
|
|
||||||
return origin + pathname + getRedactedHash(hash);
|
return origin + pathname + getRedactedHash(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,34 +57,42 @@ const customVariables = {
|
||||||
'App Platform': {
|
'App Platform': {
|
||||||
id: 1,
|
id: 1,
|
||||||
expl: _td('The platform you\'re on'),
|
expl: _td('The platform you\'re on'),
|
||||||
|
example: 'Electron Platform',
|
||||||
},
|
},
|
||||||
'App Version': {
|
'App Version': {
|
||||||
id: 2,
|
id: 2,
|
||||||
expl: _td('The version of Riot.im'),
|
expl: _td('The version of Riot.im'),
|
||||||
|
example: '15.0.0',
|
||||||
},
|
},
|
||||||
'User Type': {
|
'User Type': {
|
||||||
id: 3,
|
id: 3,
|
||||||
expl: _td('Whether or not you\'re logged in (we don\'t record your user name)'),
|
expl: _td('Whether or not you\'re logged in (we don\'t record your user name)'),
|
||||||
|
example: 'Logged In',
|
||||||
},
|
},
|
||||||
'Chosen Language': {
|
'Chosen Language': {
|
||||||
id: 4,
|
id: 4,
|
||||||
expl: _td('Your language of choice'),
|
expl: _td('Your language of choice'),
|
||||||
|
example: 'en',
|
||||||
},
|
},
|
||||||
'Instance': {
|
'Instance': {
|
||||||
id: 5,
|
id: 5,
|
||||||
expl: _td('Which officially provided instance you are using, if any'),
|
expl: _td('Which officially provided instance you are using, if any'),
|
||||||
|
example: 'app',
|
||||||
},
|
},
|
||||||
'RTE: Uses Richtext Mode': {
|
'RTE: Uses Richtext Mode': {
|
||||||
id: 6,
|
id: 6,
|
||||||
expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'),
|
expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'),
|
||||||
|
example: 'off',
|
||||||
},
|
},
|
||||||
'Homeserver URL': {
|
'Homeserver URL': {
|
||||||
id: 7,
|
id: 7,
|
||||||
expl: _td('Your homeserver\'s URL'),
|
expl: _td('Your homeserver\'s URL'),
|
||||||
|
example: 'https://matrix.org',
|
||||||
},
|
},
|
||||||
'Identity Server URL': {
|
'Identity Server URL': {
|
||||||
id: 8,
|
id: 8,
|
||||||
expl: _td('Your identity server\'s URL'),
|
expl: _td('Your identity server\'s URL'),
|
||||||
|
example: 'https://vector.im',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -183,9 +199,9 @@ class Analytics {
|
||||||
this._paq.push(['trackPageView']);
|
this._paq.push(['trackPageView']);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackEvent(category, action, name) {
|
trackEvent(category, action, name, value) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this._paq.push(['trackEvent', category, action, name]);
|
this._paq.push(['trackEvent', category, action, name, value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
|
@ -218,8 +234,19 @@ class Analytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
showDetailsModal() {
|
showDetailsModal() {
|
||||||
|
let rows = [];
|
||||||
|
if (window.Piwik) {
|
||||||
const Tracker = window.Piwik.getAsyncTracker();
|
const Tracker = window.Piwik.getAsyncTracker();
|
||||||
const rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
|
rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
|
||||||
|
} else {
|
||||||
|
// Piwik may not have been enabled, so show example values
|
||||||
|
rows = Object.keys(customVariables).map(
|
||||||
|
(k) => [
|
||||||
|
k,
|
||||||
|
_t('e.g. %(exampleValue)s', { exampleValue: customVariables[k].example }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const resolution = `${window.screen.width}x${window.screen.height}`;
|
const resolution = `${window.screen.width}x${window.screen.height}`;
|
||||||
const otherVariables = [
|
const otherVariables = [
|
||||||
|
@ -247,7 +274,7 @@ class Analytics {
|
||||||
<table>
|
<table>
|
||||||
{ rows.map((row) => <tr key={row[0]}>
|
{ rows.map((row) => <tr key={row[0]}>
|
||||||
<td>{ _t(customVariables[row[0]].expl) }</td>
|
<td>{ _t(customVariables[row[0]].expl) }</td>
|
||||||
<td><code>{ row[1] }</code></td>
|
{ row[1] !== undefined && <td><code>{ row[1] }</code></td> }
|
||||||
</tr>) }
|
</tr>) }
|
||||||
{ otherVariables.map((item, index) =>
|
{ otherVariables.map((item, index) =>
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -60,6 +60,8 @@ import { _t } from './languageHandler';
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
import dis from './dispatcher';
|
import dis from './dispatcher';
|
||||||
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
|
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
|
|
||||||
global.mxCalls = {
|
global.mxCalls = {
|
||||||
//room_id: MatrixCall
|
//room_id: MatrixCall
|
||||||
|
@ -123,7 +125,7 @@ function _setCallListeners(call) {
|
||||||
description: _t(
|
description: _t(
|
||||||
"There are unknown devices in this room: "+
|
"There are unknown devices in this room: "+
|
||||||
"if you proceed without verifying them, it will be "+
|
"if you proceed without verifying them, it will be "+
|
||||||
"possible for someone to eavesdrop on your call."
|
"possible for someone to eavesdrop on your call.",
|
||||||
),
|
),
|
||||||
button: _t('Review Devices'),
|
button: _t('Review Devices'),
|
||||||
onFinished: function(confirmed) {
|
onFinished: function(confirmed) {
|
||||||
|
@ -246,6 +248,7 @@ function _onAction(payload) {
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'place_call':
|
case 'place_call':
|
||||||
|
{
|
||||||
if (module.exports.getAnyActiveCall()) {
|
if (module.exports.getAnyActiveCall()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||||
|
@ -265,13 +268,13 @@ function _onAction(payload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var room = MatrixClientPeg.get().getRoom(payload.room_id);
|
const room = MatrixClientPeg.get().getRoom(payload.room_id);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("Room %s does not exist.", payload.room_id);
|
console.error("Room %s does not exist.", payload.room_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var members = room.getJoinedMembers();
|
const members = room.getJoinedMembers();
|
||||||
if (members.length <= 1) {
|
if (members.length <= 1) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
|
||||||
|
@ -291,9 +294,28 @@ function _onAction(payload) {
|
||||||
local_element: payload.local_element,
|
local_element: payload.local_element,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'place_conference_call':
|
case 'place_conference_call':
|
||||||
console.log("Place conference call in %s", payload.room_id);
|
console.log("Place conference call in %s", payload.room_id);
|
||||||
|
|
||||||
|
if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
|
||||||
|
// Conference calls are implemented by sending the media to central
|
||||||
|
// server which combines the audio from all the participants together
|
||||||
|
// into a single stream. This is incompatible with end-to-end encryption
|
||||||
|
// because a central server would be decrypting the audio for each
|
||||||
|
// participant.
|
||||||
|
// Therefore we disable conference calling in E2E rooms.
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
|
||||||
|
description: _t('Conference calls are not supported in encrypted rooms'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsStore.isFeatureEnabled('feature_jitsi')) {
|
||||||
|
_startCallApp(payload.room_id, payload.type);
|
||||||
|
} else {
|
||||||
if (!ConferenceHandler) {
|
if (!ConferenceHandler) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
||||||
|
@ -305,17 +327,6 @@ function _onAction(payload) {
|
||||||
title: _t('VoIP is unsupported'),
|
title: _t('VoIP is unsupported'),
|
||||||
description: _t('You cannot place VoIP calls in this browser.'),
|
description: _t('You cannot place VoIP calls in this browser.'),
|
||||||
});
|
});
|
||||||
} else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
|
|
||||||
// Conference calls are implemented by sending the media to central
|
|
||||||
// server which combines the audio from all the participants together
|
|
||||||
// into a single stream. This is incompatible with end-to-end encryption
|
|
||||||
// because a central server would be decrypting the audio for each
|
|
||||||
// participant.
|
|
||||||
// Therefore we disable conference calling in E2E rooms.
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
|
|
||||||
description: _t('Conference calls are not supported in encrypted rooms'),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
||||||
|
@ -330,17 +341,27 @@ function _onAction(payload) {
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
console.error("Conference call failed: " + err);
|
console.error("Conference call failed: " + err);
|
||||||
Modal.createTrackedDialog('Call Handler', 'Failed to set up conference call', ErrorDialog, {
|
Modal.createTrackedDialog(
|
||||||
|
'Call Handler',
|
||||||
|
'Failed to set up conference call',
|
||||||
|
ErrorDialog,
|
||||||
|
{
|
||||||
title: _t('Failed to set up conference call'),
|
title: _t('Failed to set up conference call'),
|
||||||
description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
|
description: (
|
||||||
});
|
_t('Conference call failed.') +
|
||||||
|
' ' + ((err && err.message) ? err.message : '')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'incoming_call':
|
case 'incoming_call':
|
||||||
|
{
|
||||||
if (module.exports.getAnyActiveCall()) {
|
if (module.exports.getAnyActiveCall()) {
|
||||||
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
|
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
|
||||||
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
|
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
|
||||||
|
@ -354,9 +375,10 @@ function _onAction(payload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var call = payload.call;
|
const call = payload.call;
|
||||||
_setCallListeners(call);
|
_setCallListeners(call);
|
||||||
_setCallState(call, call.roomId, "ringing");
|
_setCallState(call, call.roomId, "ringing");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'hangup':
|
case 'hangup':
|
||||||
if (!calls[payload.room_id]) {
|
if (!calls[payload.room_id]) {
|
||||||
|
@ -378,6 +400,71 @@ function _onAction(payload) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _startCallApp(roomId, type) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'appsDrawer',
|
||||||
|
show: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
console.error("Attempted to start conference call widget in unknown room: " + roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentJitsiWidgets = WidgetUtils.getRoomWidgets(room).filter((ev) => {
|
||||||
|
return ev.getContent().type === 'jitsi';
|
||||||
|
});
|
||||||
|
if (currentJitsiWidgets.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
"Refusing to start conference call widget in " + roomId +
|
||||||
|
" a conference call widget is already present",
|
||||||
|
);
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
|
Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
|
||||||
|
title: _t('Call in Progress'),
|
||||||
|
description: _t('A call is already in progress!'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This inherits its poor naming from the field of the same name that goes into
|
||||||
|
// the event. It's just a random string to make the Jitsi URLs unique.
|
||||||
|
const widgetSessionId = Math.random().toString(36).substring(2);
|
||||||
|
const confId = room.roomId.replace(/[^A-Za-z0-9]/g, '') + widgetSessionId;
|
||||||
|
// NB. we can't just encodeURICompoent all of these because the $ signs need to be there
|
||||||
|
// (but currently the only thing that needs encoding is the confId)
|
||||||
|
const queryString = [
|
||||||
|
'confId='+encodeURIComponent(confId),
|
||||||
|
'isAudioConf='+(type === 'voice' ? 'true' : 'false'),
|
||||||
|
'displayName=$matrix_display_name',
|
||||||
|
'avatarUrl=$matrix_avatar_url',
|
||||||
|
'email=$matrix_user_id',
|
||||||
|
].join('&');
|
||||||
|
const widgetUrl = (
|
||||||
|
'https://scalar.vector.im/api/widgets' +
|
||||||
|
'/jitsi.html?' +
|
||||||
|
queryString
|
||||||
|
);
|
||||||
|
|
||||||
|
const widgetData = { widgetSessionId };
|
||||||
|
|
||||||
|
const widgetId = (
|
||||||
|
'jitsi_' +
|
||||||
|
MatrixClientPeg.get().credentials.userId +
|
||||||
|
'_' +
|
||||||
|
Date.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
WidgetUtils.setRoomWidget(roomId, widgetId, 'jitsi', widgetUrl, 'Jitsi', widgetData).then(() => {
|
||||||
|
console.log('Jitsi widget added');
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: Nasty way of making sure we only register
|
// FIXME: Nasty way of making sure we only register
|
||||||
// with the dispatcher once
|
// with the dispatcher once
|
||||||
if (!global.mxCallHandler) {
|
if (!global.mxCallHandler) {
|
||||||
|
|
|
@ -22,34 +22,44 @@ export default {
|
||||||
// Only needed for Electron atm, though should work in modern browsers
|
// Only needed for Electron atm, though should work in modern browsers
|
||||||
// once permission has been granted to the webapp
|
// once permission has been granted to the webapp
|
||||||
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
|
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
|
||||||
const audioIn = [];
|
const audiooutput = [];
|
||||||
const videoIn = [];
|
const audioinput = [];
|
||||||
|
const videoinput = [];
|
||||||
|
|
||||||
if (devices.some((device) => !device.label)) return false;
|
if (devices.some((device) => !device.label)) return false;
|
||||||
|
|
||||||
devices.forEach((device) => {
|
devices.forEach((device) => {
|
||||||
switch (device.kind) {
|
switch (device.kind) {
|
||||||
case 'audioinput': audioIn.push(device); break;
|
case 'audiooutput': audiooutput.push(device); break;
|
||||||
case 'videoinput': videoIn.push(device); break;
|
case 'audioinput': audioinput.push(device); break;
|
||||||
|
case 'videoinput': videoinput.push(device); break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log("Loaded WebRTC Devices", mediaDevices);
|
// console.log("Loaded WebRTC Devices", mediaDevices);
|
||||||
return {
|
return {
|
||||||
audioinput: audioIn,
|
audiooutput,
|
||||||
videoinput: videoIn,
|
audioinput,
|
||||||
|
videoinput,
|
||||||
};
|
};
|
||||||
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
|
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
|
||||||
},
|
},
|
||||||
|
|
||||||
loadDevices: function() {
|
loadDevices: function() {
|
||||||
|
const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput");
|
||||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||||
|
|
||||||
|
Matrix.setMatrixCallAudioOutput(audioOutDeviceId);
|
||||||
Matrix.setMatrixCallAudioInput(audioDeviceId);
|
Matrix.setMatrixCallAudioInput(audioDeviceId);
|
||||||
Matrix.setMatrixCallVideoInput(videoDeviceId);
|
Matrix.setMatrixCallVideoInput(videoDeviceId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setAudioOutput: function(deviceId) {
|
||||||
|
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||||
|
Matrix.setMatrixCallAudioOutput(deviceId);
|
||||||
|
},
|
||||||
|
|
||||||
setAudioInput: function(deviceId) {
|
setAudioInput: function(deviceId) {
|
||||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
Matrix.setMatrixCallAudioInput(deviceId);
|
Matrix.setMatrixCallAudioInput(deviceId);
|
||||||
|
|
|
@ -243,6 +243,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
||||||
const blob = new Blob([encryptResult.data]);
|
const blob = new Blob([encryptResult.data]);
|
||||||
return matrixClient.uploadContent(blob, {
|
return matrixClient.uploadContent(blob, {
|
||||||
progressHandler: progressHandler,
|
progressHandler: progressHandler,
|
||||||
|
includeFilename: false,
|
||||||
}).then(function(url) {
|
}).then(function(url) {
|
||||||
// If the attachment is encrypted then bundle the URL along
|
// If the attachment is encrypted then bundle the URL along
|
||||||
// with the information needed to decrypt the attachment and
|
// with the information needed to decrypt the attachment and
|
||||||
|
|
202
src/DecryptionFailureTracker.js
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class DecryptionFailure {
|
||||||
|
constructor(failedEventId, errorCode) {
|
||||||
|
this.failedEventId = failedEventId;
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.ts = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DecryptionFailureTracker {
|
||||||
|
// Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
|
||||||
|
// is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
|
||||||
|
// are accumulated in `failureCounts`.
|
||||||
|
failures = [];
|
||||||
|
|
||||||
|
// A histogram of the number of failures that will be tracked at the next tracking
|
||||||
|
// interval, split by failure error code.
|
||||||
|
failureCounts = {
|
||||||
|
// [errorCode]: 42
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event IDs of failures that were tracked previously
|
||||||
|
trackedEventHashMap = {
|
||||||
|
// [eventId]: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set to an interval ID when `start` is called
|
||||||
|
checkInterval = null;
|
||||||
|
trackInterval = null;
|
||||||
|
|
||||||
|
// Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
|
||||||
|
static TRACK_INTERVAL_MS = 60000;
|
||||||
|
|
||||||
|
// Call `checkFailures` every `CHECK_INTERVAL_MS`.
|
||||||
|
static CHECK_INTERVAL_MS = 5000;
|
||||||
|
|
||||||
|
// Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before counting
|
||||||
|
// the failure in `failureCounts`.
|
||||||
|
static GRACE_PERIOD_MS = 60000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DecryptionFailureTracker.
|
||||||
|
*
|
||||||
|
* Call `eventDecrypted(event, err)` on this instance when an event is decrypted.
|
||||||
|
*
|
||||||
|
* Call `start()` to start the tracker, and `stop()` to stop tracking.
|
||||||
|
*
|
||||||
|
* @param {function} fn The tracking function, which will be called when failures
|
||||||
|
* are tracked. The function should have a signature `(count, trackedErrorCode) => {...}`,
|
||||||
|
* where `count` is the number of failures and `errorCode` matches the `.code` of
|
||||||
|
* provided DecryptionError errors (by default, unless `errorCodeMapFn` is specified.
|
||||||
|
* @param {function?} errorCodeMapFn The function used to map error codes to the
|
||||||
|
* trackedErrorCode. If not provided, the `.code` of errors will be used.
|
||||||
|
*/
|
||||||
|
constructor(fn, errorCodeMapFn) {
|
||||||
|
if (!fn || typeof fn !== 'function') {
|
||||||
|
throw new Error('DecryptionFailureTracker requires tracking function');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') {
|
||||||
|
throw new Error('DecryptionFailureTracker second constructor argument should be a function');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._trackDecryptionFailure = fn;
|
||||||
|
this._mapErrorCode = errorCodeMapFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadTrackedEventHashMap() {
|
||||||
|
// this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {};
|
||||||
|
// }
|
||||||
|
|
||||||
|
// saveTrackedEventHashMap() {
|
||||||
|
// localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
|
||||||
|
// }
|
||||||
|
|
||||||
|
eventDecrypted(e, err) {
|
||||||
|
if (err) {
|
||||||
|
this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code));
|
||||||
|
} else {
|
||||||
|
// Could be an event in the failures, remove it
|
||||||
|
this.removeDecryptionFailuresForEvent(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addDecryptionFailure(failure) {
|
||||||
|
this.failures.push(failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDecryptionFailuresForEvent(e) {
|
||||||
|
this.failures = this.failures.filter((f) => f.failedEventId !== e.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start checking for and tracking failures.
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
this.checkInterval = setInterval(
|
||||||
|
() => this.checkFailures(Date.now()),
|
||||||
|
DecryptionFailureTracker.CHECK_INTERVAL_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.trackInterval = setInterval(
|
||||||
|
() => this.trackFailures(),
|
||||||
|
DecryptionFailureTracker.TRACK_INTERVAL_MS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear state and stop checking for and tracking failures.
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
clearInterval(this.checkInterval);
|
||||||
|
clearInterval(this.trackInterval);
|
||||||
|
|
||||||
|
this.failures = [];
|
||||||
|
this.failureCounts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be
|
||||||
|
* tracked. Only mark one failure per event ID.
|
||||||
|
* @param {number} nowTs the timestamp that represents the time now.
|
||||||
|
*/
|
||||||
|
checkFailures(nowTs) {
|
||||||
|
const failuresGivenGrace = [];
|
||||||
|
const failuresNotReady = [];
|
||||||
|
while (this.failures.length > 0) {
|
||||||
|
const f = this.failures.shift();
|
||||||
|
if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) {
|
||||||
|
failuresGivenGrace.push(f);
|
||||||
|
} else {
|
||||||
|
failuresNotReady.push(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.failures = failuresNotReady;
|
||||||
|
|
||||||
|
// Only track one failure per event
|
||||||
|
const dedupedFailuresMap = failuresGivenGrace.reduce(
|
||||||
|
(map, failure) => {
|
||||||
|
if (!this.trackedEventHashMap[failure.failedEventId]) {
|
||||||
|
return map.set(failure.failedEventId, failure);
|
||||||
|
} else {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Use a map to preseve key ordering
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const trackedEventIds = [...dedupedFailuresMap.keys()];
|
||||||
|
|
||||||
|
this.trackedEventHashMap = trackedEventIds.reduce(
|
||||||
|
(result, eventId) => ({...result, [eventId]: true}),
|
||||||
|
this.trackedEventHashMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Commented out for now for expediency, we need to consider unbound nature of storing
|
||||||
|
// this in localStorage
|
||||||
|
// this.saveTrackedEventHashMap();
|
||||||
|
|
||||||
|
const dedupedFailures = dedupedFailuresMap.values();
|
||||||
|
|
||||||
|
this._aggregateFailures(dedupedFailures);
|
||||||
|
}
|
||||||
|
|
||||||
|
_aggregateFailures(failures) {
|
||||||
|
for (const failure of failures) {
|
||||||
|
const errorCode = failure.errorCode;
|
||||||
|
this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there are failures that should be tracked, call the given trackDecryptionFailure
|
||||||
|
* function with the number of failures that should be tracked.
|
||||||
|
*/
|
||||||
|
trackFailures() {
|
||||||
|
for (const errorCode of Object.keys(this.failureCounts)) {
|
||||||
|
if (this.failureCounts[errorCode] > 0) {
|
||||||
|
const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode;
|
||||||
|
|
||||||
|
this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode);
|
||||||
|
this.failureCounts[errorCode] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import sdk from './';
|
import sdk from './';
|
||||||
import MultiInviter from './utils/MultiInviter';
|
import MultiInviter from './utils/MultiInviter';
|
||||||
|
|
|
@ -217,10 +217,17 @@ const sanitizeHtmlParams = {
|
||||||
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
|
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
|
||||||
if (m) {
|
if (m) {
|
||||||
const entity = m[1];
|
const entity = m[1];
|
||||||
if (entity[0] === '@') {
|
switch (entity[0]) {
|
||||||
|
case '@':
|
||||||
attribs.href = '#/user/' + entity;
|
attribs.href = '#/user/' + entity;
|
||||||
} else if (entity[0] === '#' || entity[0] === '!') {
|
break;
|
||||||
|
case '+':
|
||||||
|
attribs.href = '#/group/' + entity;
|
||||||
|
break;
|
||||||
|
case '#':
|
||||||
|
case '!':
|
||||||
attribs.href = '#/room/' + entity;
|
attribs.href = '#/room/' + entity;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
delete attribs.target;
|
delete attribs.target;
|
||||||
}
|
}
|
||||||
|
|
42
src/Modal.js
|
@ -81,7 +81,11 @@ class ModalManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._counter = 0;
|
this._counter = 0;
|
||||||
|
|
||||||
/** list of the modals we have stacked up, with the most recent at [0] */
|
// The modal to prioritise over all others. If this is set, only show
|
||||||
|
// this modal. Remove all other modals from the stack when this modal
|
||||||
|
// is closed.
|
||||||
|
this._priorityModal = null;
|
||||||
|
// A list of the modals we have stacked up, with the most recent at [0]
|
||||||
this._modals = [
|
this._modals = [
|
||||||
/* {
|
/* {
|
||||||
elem: React component for this dialog
|
elem: React component for this dialog
|
||||||
|
@ -105,18 +109,18 @@ class ModalManager {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
createTrackedDialog(analyticsAction, analyticsInfo, Element, props, className) {
|
createTrackedDialog(analyticsAction, analyticsInfo, ...rest) {
|
||||||
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
||||||
return this.createDialog(Element, props, className);
|
return this.createDialog(...rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
createDialog(Element, props, className) {
|
createDialog(Element, ...rest) {
|
||||||
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
|
return this.createDialogAsync((cb) => {cb(Element);}, ...rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
createTrackedDialogAsync(analyticsAction, analyticsInfo, loader, props, className) {
|
createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) {
|
||||||
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
||||||
return this.createDialogAsync(loader, props, className);
|
return this.createDialogAsync(...rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -137,8 +141,13 @@ class ModalManager {
|
||||||
* component. (We will also pass an 'onFinished' property.)
|
* component. (We will also pass an 'onFinished' property.)
|
||||||
*
|
*
|
||||||
* @param {String} className CSS class to apply to the modal wrapper
|
* @param {String} className CSS class to apply to the modal wrapper
|
||||||
|
*
|
||||||
|
* @param {boolean} isPriorityModal if true, this modal will be displayed regardless
|
||||||
|
* of other modals that are currently in the stack.
|
||||||
|
* Also, when closed, all modals will be removed
|
||||||
|
* from the stack.
|
||||||
*/
|
*/
|
||||||
createDialogAsync(loader, props, className) {
|
createDialogAsync(loader, props, className, isPriorityModal) {
|
||||||
const self = this;
|
const self = this;
|
||||||
const modal = {};
|
const modal = {};
|
||||||
|
|
||||||
|
@ -151,6 +160,14 @@ class ModalManager {
|
||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
self._modals.splice(i, 1);
|
self._modals.splice(i, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self._priorityModal === modal) {
|
||||||
|
self._priorityModal = null;
|
||||||
|
|
||||||
|
// XXX: This is destructive
|
||||||
|
self._modals = [];
|
||||||
|
}
|
||||||
|
|
||||||
self._reRender();
|
self._reRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -167,7 +184,12 @@ class ModalManager {
|
||||||
modal.onFinished = props ? props.onFinished : null;
|
modal.onFinished = props ? props.onFinished : null;
|
||||||
modal.className = className;
|
modal.className = className;
|
||||||
|
|
||||||
|
if (isPriorityModal) {
|
||||||
|
// XXX: This is destructive
|
||||||
|
this._priorityModal = modal;
|
||||||
|
} else {
|
||||||
this._modals.unshift(modal);
|
this._modals.unshift(modal);
|
||||||
|
}
|
||||||
|
|
||||||
this._reRender();
|
this._reRender();
|
||||||
return {close: closeDialog};
|
return {close: closeDialog};
|
||||||
|
@ -188,7 +210,7 @@ class ModalManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
_reRender() {
|
_reRender() {
|
||||||
if (this._modals.length == 0) {
|
if (this._modals.length == 0 && !this._priorityModal) {
|
||||||
// If there is no modal to render, make all of Riot available
|
// If there is no modal to render, make all of Riot available
|
||||||
// to screen reader users again
|
// to screen reader users again
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -205,7 +227,7 @@ class ModalManager {
|
||||||
action: 'aria_hide_main_app',
|
action: 'aria_hide_main_app',
|
||||||
});
|
});
|
||||||
|
|
||||||
const modal = this._modals[0];
|
const modal = this._priorityModal ? this._priorityModal : this._modals[0];
|
||||||
const dialog = (
|
const dialog = (
|
||||||
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
|
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
|
|
|
@ -170,15 +170,15 @@ const Notifier = {
|
||||||
value: true,
|
value: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// clear the notifications_hidden flag, so that if notifications are
|
|
||||||
// disabled again in the future, we will show the banner again.
|
|
||||||
this.setToolbarHidden(true);
|
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
value: false,
|
value: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// set the notifications_hidden flag, as the user has knowingly interacted
|
||||||
|
// with the setting we shouldn't nag them any further
|
||||||
|
this.setToolbarHidden(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
isEnabled: function() {
|
isEnabled: function() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -231,11 +232,12 @@ Example:
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SdkConfig = require('./SdkConfig');
|
import SdkConfig from './SdkConfig';
|
||||||
const MatrixClientPeg = require("./MatrixClientPeg");
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
|
import { MatrixEvent } from 'matrix-js-sdk';
|
||||||
const dis = require("./dispatcher");
|
import dis from './dispatcher';
|
||||||
const Widgets = require('./utils/widgets');
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
|
import RoomViewStore from './stores/RoomViewStore';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
function sendResponse(event, res) {
|
function sendResponse(event, res) {
|
||||||
|
@ -286,51 +288,6 @@ function inviteUser(event, roomId, userId) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a promise that resolves when a widget with the given
|
|
||||||
* ID has been added as a user widget (ie. the accountData event
|
|
||||||
* arrives) or rejects after a timeout
|
|
||||||
*
|
|
||||||
* @param {string} widgetId The ID of the widget to wait for
|
|
||||||
* @param {boolean} add True to wait for the widget to be added,
|
|
||||||
* false to wait for it to be deleted.
|
|
||||||
* @returns {Promise} that resolves when the widget is available
|
|
||||||
*/
|
|
||||||
function waitForUserWidget(widgetId, add) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const currentAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets');
|
|
||||||
|
|
||||||
// Tests an account data event, returning true if it's in the state
|
|
||||||
// we're waiting for it to be in
|
|
||||||
function eventInIntendedState(ev) {
|
|
||||||
if (!ev || !currentAccountDataEvent.getContent()) return false;
|
|
||||||
if (add) {
|
|
||||||
return ev.getContent()[widgetId] !== undefined;
|
|
||||||
} else {
|
|
||||||
return ev.getContent()[widgetId] === undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventInIntendedState(currentAccountDataEvent)) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAccountData(ev) {
|
|
||||||
if (eventInIntendedState(currentAccountDataEvent)) {
|
|
||||||
MatrixClientPeg.get().removeListener('accountData', onAccountData);
|
|
||||||
clearTimeout(timerId);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const timerId = setTimeout(() => {
|
|
||||||
MatrixClientPeg.get().removeListener('accountData', onAccountData);
|
|
||||||
reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear"));
|
|
||||||
}, 10000);
|
|
||||||
MatrixClientPeg.get().on('accountData', onAccountData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWidget(event, roomId) {
|
function setWidget(event, roomId) {
|
||||||
const widgetId = event.data.widget_id;
|
const widgetId = event.data.widget_id;
|
||||||
const widgetType = event.data.type;
|
const widgetType = event.data.type;
|
||||||
|
@ -339,12 +296,6 @@ function setWidget(event, roomId) {
|
||||||
const widgetData = event.data.data; // optional
|
const widgetData = event.data.data; // optional
|
||||||
const userWidget = event.data.userWidget;
|
const userWidget = event.data.userWidget;
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
|
||||||
if (!client) {
|
|
||||||
sendError(event, _t('You need to be logged in.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// both adding/removing widgets need these checks
|
// both adding/removing widgets need these checks
|
||||||
if (!widgetId || widgetUrl === undefined) {
|
if (!widgetId || widgetUrl === undefined) {
|
||||||
sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields."));
|
sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields."));
|
||||||
|
@ -371,42 +322,8 @@ function setWidget(event, roomId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = {
|
|
||||||
type: widgetType,
|
|
||||||
url: widgetUrl,
|
|
||||||
name: widgetName,
|
|
||||||
data: widgetData,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (userWidget) {
|
if (userWidget) {
|
||||||
const client = MatrixClientPeg.get();
|
WidgetUtils.setUserWidget(widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => {
|
||||||
const userWidgets = Widgets.getUserWidgets();
|
|
||||||
|
|
||||||
// Delete existing widget with ID
|
|
||||||
try {
|
|
||||||
delete userWidgets[widgetId];
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`$widgetId is non-configurable`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new widget / update
|
|
||||||
if (widgetUrl !== null) {
|
|
||||||
userWidgets[widgetId] = {
|
|
||||||
content: content,
|
|
||||||
sender: client.getUserId(),
|
|
||||||
state_key: widgetId,
|
|
||||||
type: 'm.widget',
|
|
||||||
id: widgetId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// This starts listening for when the echo comes back from the server
|
|
||||||
// since the widget won't appear added until this happens. If we don't
|
|
||||||
// wait for this, the action will complete but if the user is fast enough,
|
|
||||||
// the widget still won't actually be there.
|
|
||||||
client.setAccountData('m.widgets', userWidgets).then(() => {
|
|
||||||
return waitForUserWidget(widgetId, widgetUrl !== null);
|
|
||||||
}).then(() => {
|
|
||||||
sendResponse(event, {
|
sendResponse(event, {
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
|
@ -419,15 +336,7 @@ function setWidget(event, roomId) {
|
||||||
if (!roomId) {
|
if (!roomId) {
|
||||||
sendError(event, _t('Missing roomId.'), null);
|
sendError(event, _t('Missing roomId.'), null);
|
||||||
}
|
}
|
||||||
|
WidgetUtils.setRoomWidget(roomId, widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => {
|
||||||
if (widgetUrl === null) { // widget is being deleted
|
|
||||||
content = {};
|
|
||||||
}
|
|
||||||
// TODO - Room widgets need to be moved to 'm.widget' state events
|
|
||||||
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
|
|
||||||
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
|
|
||||||
// XXX: We should probably wait for the echo of the state event to come back from the server,
|
|
||||||
// as we do with user widgets.
|
|
||||||
sendResponse(event, {
|
sendResponse(event, {
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
|
@ -451,21 +360,13 @@ function getWidgets(event, roomId) {
|
||||||
sendError(event, _t('This room is not recognised.'));
|
sendError(event, _t('This room is not recognised.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO - Room widgets need to be moved to 'm.widget' state events
|
// XXX: This gets the raw event object (I think because we can't
|
||||||
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
|
// send the MatrixEvent over postMessage?)
|
||||||
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
widgetStateEvents = WidgetUtils.getRoomWidgets(room).map((ev) => ev.event);
|
||||||
// Only return widgets which have required fields
|
|
||||||
if (room) {
|
|
||||||
stateEvents.forEach((ev) => {
|
|
||||||
if (ev.getContent().type && ev.getContent().url) {
|
|
||||||
widgetStateEvents.push(ev.event); // return the raw event
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user widgets (not linked to a specific room)
|
// Add user widgets (not linked to a specific room)
|
||||||
const userWidgets = Widgets.getUserWidgetsArray();
|
const userWidgets = WidgetUtils.getUserWidgetsArray();
|
||||||
widgetStateEvents = widgetStateEvents.concat(userWidgets);
|
widgetStateEvents = widgetStateEvents.concat(userWidgets);
|
||||||
|
|
||||||
sendResponse(event, widgetStateEvents);
|
sendResponse(event, widgetStateEvents);
|
||||||
|
@ -637,19 +538,6 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||||
sendResponse(event, stateEvent.getContent());
|
sendResponse(event, stateEvent.getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentRoomId = null;
|
|
||||||
let currentRoomAlias = null;
|
|
||||||
|
|
||||||
// Listen for when a room is viewed
|
|
||||||
dis.register(onAction);
|
|
||||||
function onAction(payload) {
|
|
||||||
if (payload.action !== "view_room") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
currentRoomId = payload.room_id;
|
|
||||||
currentRoomAlias = payload.room_alias;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMessage = function(event) {
|
const onMessage = function(event) {
|
||||||
if (!event.origin) { // stupid chrome
|
if (!event.origin) { // stupid chrome
|
||||||
event.origin = event.originalEvent.origin;
|
event.origin = event.originalEvent.origin;
|
||||||
|
@ -700,21 +588,8 @@ const onMessage = function(event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let promise = Promise.resolve(currentRoomId);
|
|
||||||
if (!currentRoomId) {
|
|
||||||
if (!currentRoomAlias) {
|
|
||||||
sendError(event, _t('Must be viewing a room'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// no room ID but there is an alias, look it up.
|
|
||||||
console.log("Looking up alias " + currentRoomAlias);
|
|
||||||
promise = MatrixClientPeg.get().getRoomIdForAlias(currentRoomAlias).then((res) => {
|
|
||||||
return res.room_id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.then((viewingRoomId) => {
|
if (roomId !== RoomViewStore.getRoomId()) {
|
||||||
if (roomId !== viewingRoomId) {
|
|
||||||
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
|
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -770,10 +645,6 @@ const onMessage = function(event) {
|
||||||
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, (err) => {
|
|
||||||
console.error(err);
|
|
||||||
sendError(event, _t('Failed to lookup current room') + '.');
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let listenerCount = 0;
|
let listenerCount = 0;
|
||||||
|
|
|
@ -14,28 +14,31 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MatrixClientPeg from "./MatrixClientPeg";
|
|
||||||
import dis from "./dispatcher";
|
import React from 'react';
|
||||||
import Tinter from "./Tinter";
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
import dis from './dispatcher';
|
||||||
|
import Tinter from './Tinter';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import {_t, _td} from './languageHandler';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from './settings/SettingsStore';
|
||||||
|
|
||||||
|
|
||||||
class Command {
|
class Command {
|
||||||
constructor(name, paramArgs, runFn) {
|
constructor({name, args='', description, runFn}) {
|
||||||
this.name = name;
|
this.command = '/' + name;
|
||||||
this.paramArgs = paramArgs;
|
this.args = args;
|
||||||
|
this.description = description;
|
||||||
this.runFn = runFn;
|
this.runFn = runFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommand() {
|
getCommand() {
|
||||||
return "/" + this.name;
|
return this.command;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommandWithArgs() {
|
getCommandWithArgs() {
|
||||||
return this.getCommand() + " " + this.paramArgs;
|
return this.getCommand() + " " + this.args;
|
||||||
}
|
}
|
||||||
|
|
||||||
run(roomId, args) {
|
run(roomId, args) {
|
||||||
|
@ -47,16 +50,12 @@ class Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reject(msg) {
|
function reject(error) {
|
||||||
return {
|
return {error};
|
||||||
error: msg,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function success(promise) {
|
function success(promise) {
|
||||||
return {
|
return {promise};
|
||||||
promise: promise,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disable the "unexpected this" error for these commands - all of the run
|
/* Disable the "unexpected this" error for these commands - all of the run
|
||||||
|
@ -65,8 +64,12 @@ function success(promise) {
|
||||||
|
|
||||||
/* eslint-disable babel/no-invalid-this */
|
/* eslint-disable babel/no-invalid-this */
|
||||||
|
|
||||||
const commands = {
|
export const CommandMap = {
|
||||||
ddg: new Command("ddg", "<query>", function(roomId, args) {
|
ddg: new Command({
|
||||||
|
name: 'ddg',
|
||||||
|
args: '<query>',
|
||||||
|
description: _td('Searches DuckDuckGo for results'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
// TODO Don't explain this away, actually show a search UI here.
|
// TODO Don't explain this away, actually show a search UI here.
|
||||||
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
|
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
|
||||||
|
@ -74,22 +77,28 @@ const commands = {
|
||||||
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
||||||
});
|
});
|
||||||
return success();
|
return success();
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Change your nickname
|
nick: new Command({
|
||||||
nick: new Command("nick", "<display_name>", function(roomId, args) {
|
name: 'nick',
|
||||||
|
args: '<display_name>',
|
||||||
|
description: _td('Changes your display nickname'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
return success(
|
return success(MatrixClientPeg.get().setDisplayName(args));
|
||||||
MatrixClientPeg.get().setDisplayName(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Changes the colorscheme of your current room
|
tint: new Command({
|
||||||
tint: new Command("tint", "<color1> [<color2>]", function(roomId, args) {
|
name: 'tint',
|
||||||
|
args: '<color1> [<color2>]',
|
||||||
|
description: _td('Changes colour scheme of current room'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const 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(/^(#([\da-fA-F]{3}|[\da-fA-F]{6}))( +(#([\da-fA-F]{3}|[\da-fA-F]{6})))?$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
Tinter.tint(matches[1], matches[4]);
|
Tinter.tint(matches[1], matches[4]);
|
||||||
const colorScheme = {};
|
const colorScheme = {};
|
||||||
|
@ -100,46 +109,53 @@ const commands = {
|
||||||
colorScheme.secondary_color = colorScheme.primary_color;
|
colorScheme.secondary_color = colorScheme.primary_color;
|
||||||
}
|
}
|
||||||
return success(
|
return success(
|
||||||
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
SettingsStore.setValue('roomColor', roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Change the room topic
|
topic: new Command({
|
||||||
topic: new Command("topic", "<topic>", function(roomId, args) {
|
name: 'topic',
|
||||||
|
args: '<topic>',
|
||||||
|
description: _td('Sets the room topic'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
return success(
|
return success(MatrixClientPeg.get().setRoomTopic(roomId, args));
|
||||||
MatrixClientPeg.get().setRoomTopic(roomId, args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Invite a user
|
invite: new Command({
|
||||||
invite: new Command("invite", "<userId>", function(roomId, args) {
|
name: 'invite',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Invites user with given id to current room'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
return success(
|
return success(MatrixClientPeg.get().invite(roomId, matches[1]));
|
||||||
MatrixClientPeg.get().invite(roomId, matches[1]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Join a room
|
join: new Command({
|
||||||
join: new Command("join", "#alias:domain", function(roomId, args) {
|
name: 'join',
|
||||||
|
args: '<room-alias>',
|
||||||
|
description: _td('Joins room with given alias'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
let roomAlias = matches[1];
|
let roomAlias = matches[1];
|
||||||
if (roomAlias[0] !== '#') {
|
if (roomAlias[0] !== '#') return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
|
||||||
}
|
if (!roomAlias.includes(':')) {
|
||||||
if (!roomAlias.match(/:/)) {
|
|
||||||
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,27 +169,31 @@ const commands = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
part: new Command("part", "[#alias:domain]", function(roomId, args) {
|
part: new Command({
|
||||||
|
name: 'part',
|
||||||
|
args: '[<room-alias>]',
|
||||||
|
description: _td('Leave room'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
let targetRoomId;
|
let targetRoomId;
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
let roomAlias = matches[1];
|
let roomAlias = matches[1];
|
||||||
if (roomAlias[0] !== '#') {
|
if (roomAlias[0] !== '#') return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
|
||||||
}
|
if (!roomAlias.includes(':')) {
|
||||||
if (!roomAlias.match(/:/)) {
|
roomAlias += ':' + cli.getDomain();
|
||||||
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find a room with this alias
|
// Try to find a room with this alias
|
||||||
const rooms = MatrixClientPeg.get().getRooms();
|
const rooms = cli.getRooms();
|
||||||
for (let i = 0; i < rooms.length; i++) {
|
for (let i = 0; i < rooms.length; i++) {
|
||||||
const aliasEvents = rooms[i].currentState.getStateEvents(
|
const aliasEvents = rooms[i].currentState.getStateEvents('m.room.aliases');
|
||||||
"m.room.aliases",
|
|
||||||
);
|
|
||||||
for (let j = 0; j < aliasEvents.length; j++) {
|
for (let j = 0; j < aliasEvents.length; j++) {
|
||||||
const aliases = aliasEvents[j].getContent().aliases || [];
|
const aliases = aliasEvents[j].getContent().aliases || [];
|
||||||
for (let k = 0; k < aliases.length; k++) {
|
for (let k = 0; k < aliases.length; k++) {
|
||||||
|
@ -182,82 +202,92 @@ const commands = {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (targetRoomId) { break; }
|
if (targetRoomId) break;
|
||||||
}
|
}
|
||||||
if (targetRoomId) { break; }
|
if (targetRoomId) break;
|
||||||
}
|
|
||||||
if (!targetRoomId) {
|
|
||||||
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
|
|
||||||
}
|
}
|
||||||
|
if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!targetRoomId) targetRoomId = roomId;
|
if (!targetRoomId) targetRoomId = roomId;
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().leave(targetRoomId).then(
|
cli.leave(targetRoomId).then(function() {
|
||||||
function() {
|
|
||||||
dis.dispatch({action: 'view_next_room'});
|
dis.dispatch({action: 'view_next_room'});
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Kick a user from the room with an optional reason
|
kick: new Command({
|
||||||
kick: new Command("kick", "<userId> [<reason>]", function(roomId, args) {
|
name: 'kick',
|
||||||
|
args: '<user-id> [reason]',
|
||||||
|
description: _td('Kicks user with given id'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
return success(
|
return success(MatrixClientPeg.get().kick(roomId, matches[1], matches[3]));
|
||||||
MatrixClientPeg.get().kick(roomId, matches[1], matches[3]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Ban a user from the room with an optional reason
|
// Ban a user from the room with an optional reason
|
||||||
ban: new Command("ban", "<userId> [<reason>]", function(roomId, args) {
|
ban: new Command({
|
||||||
|
name: 'ban',
|
||||||
|
args: '<user-id> [reason]',
|
||||||
|
description: _td('Bans user with given id'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
return success(
|
return success(MatrixClientPeg.get().ban(roomId, matches[1], matches[3]));
|
||||||
MatrixClientPeg.get().ban(roomId, matches[1], matches[3]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Unban a user from the room
|
// Unban a user from ythe room
|
||||||
unban: new Command("unban", "<userId>", function(roomId, args) {
|
unban: new Command({
|
||||||
|
name: 'unban',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Unbans user with given id'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
// Reset the user membership to "leave" to unban him
|
// Reset the user membership to "leave" to unban him
|
||||||
return success(
|
return success(MatrixClientPeg.get().unban(roomId, matches[1]));
|
||||||
MatrixClientPeg.get().unban(roomId, matches[1]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ignore: new Command("ignore", "<userId>", function(roomId, args) {
|
ignore: new Command({
|
||||||
|
name: 'ignore',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Ignores a user, hiding their messages from you'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
const matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const userId = matches[1];
|
const userId = matches[1];
|
||||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
const ignoredUsers = cli.getIgnoredUsers();
|
||||||
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
||||||
title: _t("Ignored user"),
|
title: _t('Ignored user'),
|
||||||
description: (
|
description: <div>
|
||||||
<div>
|
<p>{ _t('You are now ignoring %(userId)s', {userId}) }</p>
|
||||||
<p>{ _t("You are now ignoring %(userId)s", {userId: userId}) }</p>
|
</div>,
|
||||||
</div>
|
|
||||||
),
|
|
||||||
hasCancelButton: false,
|
hasCancelButton: false,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
@ -265,26 +295,31 @@ const commands = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
unignore: new Command("unignore", "<userId>", function(roomId, args) {
|
unignore: new Command({
|
||||||
|
name: 'unignore',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Stops ignoring a user, showing their messages going forward'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
const matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const userId = matches[1];
|
const userId = matches[1];
|
||||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
const ignoredUsers = cli.getIgnoredUsers();
|
||||||
const index = ignoredUsers.indexOf(userId);
|
const index = ignoredUsers.indexOf(userId);
|
||||||
if (index !== -1) ignoredUsers.splice(index, 1);
|
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
||||||
title: _t("Unignored user"),
|
title: _t('Unignored user'),
|
||||||
description: (
|
description: <div>
|
||||||
<div>
|
<p>{ _t('You are no longer ignoring %(userId)s', {userId}) }</p>
|
||||||
<p>{ _t("You are no longer ignoring %(userId)s", {userId: userId}) }</p>
|
</div>,
|
||||||
</div>
|
|
||||||
),
|
|
||||||
hasCancelButton: false,
|
hasCancelButton: false,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
@ -292,10 +327,15 @@ const commands = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Define the power level of a user
|
// Define the power level of a user
|
||||||
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
op: new Command({
|
||||||
|
name: 'op',
|
||||||
|
args: '<user-id> [<power-level>]',
|
||||||
|
description: _td('Define the power level of a user'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
||||||
let powerLevel = 50; // default power level for op
|
let powerLevel = 50; // default power level for op
|
||||||
|
@ -305,59 +345,61 @@ const commands = {
|
||||||
powerLevel = parseInt(matches[3]);
|
powerLevel = parseInt(matches[3]);
|
||||||
}
|
}
|
||||||
if (!isNaN(powerLevel)) {
|
if (!isNaN(powerLevel)) {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const cli = MatrixClientPeg.get();
|
||||||
if (!room) {
|
const room = cli.getRoom(roomId);
|
||||||
return reject("Bad room ID: " + roomId);
|
if (!room) return reject('Bad room ID: ' + roomId);
|
||||||
}
|
|
||||||
const powerLevelEvent = room.currentState.getStateEvents(
|
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||||
"m.room.power_levels", "",
|
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
|
||||||
);
|
|
||||||
return success(
|
|
||||||
MatrixClientPeg.get().setPowerLevel(
|
|
||||||
roomId, userId, powerLevel, powerLevelEvent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Reset the power level of a user
|
// Reset the power level of a user
|
||||||
deop: new Command("deop", "<userId>", function(roomId, args) {
|
deop: new Command({
|
||||||
|
name: 'deop',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Deops user with given id'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+)$/);
|
const matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const cli = MatrixClientPeg.get();
|
||||||
if (!room) {
|
const room = cli.getRoom(roomId);
|
||||||
return reject("Bad room ID: " + roomId);
|
if (!room) return reject('Bad room ID: ' + roomId);
|
||||||
}
|
|
||||||
|
|
||||||
const powerLevelEvent = room.currentState.getStateEvents(
|
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||||
"m.room.power_levels", "",
|
return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
|
||||||
);
|
|
||||||
return success(
|
|
||||||
MatrixClientPeg.get().setPowerLevel(
|
|
||||||
roomId, args, undefined, powerLevelEvent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Open developer tools
|
devtools: new Command({
|
||||||
devtools: new Command("devtools", "", function(roomId) {
|
name: 'devtools',
|
||||||
const DevtoolsDialog = sdk.getComponent("dialogs.DevtoolsDialog");
|
description: _td('Opens the Developer Tools dialog'),
|
||||||
|
runFn: function(roomId) {
|
||||||
|
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
||||||
Modal.createDialog(DevtoolsDialog, {roomId});
|
Modal.createDialog(DevtoolsDialog, {roomId});
|
||||||
return success();
|
return success();
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Verify a user, device, and pubkey tuple
|
// Verify a user, device, and pubkey tuple
|
||||||
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
|
verify: new Command({
|
||||||
|
name: 'verify',
|
||||||
|
args: '<user-id> <device-id> <device-signing-key>',
|
||||||
|
description: _td('Verifies a user, device, and pubkey tuple'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
|
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
const userId = matches[1];
|
const userId = matches[1];
|
||||||
const deviceId = matches[2];
|
const deviceId = matches[2];
|
||||||
const fingerprint = matches[3];
|
const fingerprint = matches[3];
|
||||||
|
@ -365,16 +407,16 @@ const commands = {
|
||||||
return success(
|
return success(
|
||||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||||
// in future
|
// in future
|
||||||
Promise.resolve(MatrixClientPeg.get().getStoredDevice(userId, deviceId)).then((device) => {
|
Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => {
|
||||||
if (!device) {
|
if (!device) {
|
||||||
throw new Error(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
|
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device.isVerified()) {
|
if (device.isVerified()) {
|
||||||
if (device.getFingerprint() === fingerprint) {
|
if (device.getFingerprint() === fingerprint) {
|
||||||
throw new Error(_t(`Device already verified!`));
|
throw new Error(_t('Device already verified!'));
|
||||||
} else {
|
} else {
|
||||||
throw new Error(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
|
throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,26 +426,29 @@ const commands = {
|
||||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
_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 ' +
|
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
|
||||||
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||||
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}));
|
{
|
||||||
|
fprint,
|
||||||
|
userId,
|
||||||
|
deviceId,
|
||||||
|
fingerprint,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true);
|
return cli.setDeviceVerified(userId, deviceId, true);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// Tell the user we verified everything
|
// Tell the user we verified everything
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
||||||
title: _t("Verified key"),
|
title: _t('Verified key'),
|
||||||
description: (
|
description: <div>
|
||||||
<div>
|
|
||||||
<p>
|
<p>
|
||||||
{
|
{
|
||||||
_t("The signing key you provided matches the signing key you received " +
|
_t('The signing key you provided matches the signing key you received ' +
|
||||||
"from %(userId)s's device %(deviceId)s. Device marked as verified.",
|
'from %(userId)s\'s device %(deviceId)s. Device marked as verified.',
|
||||||
{userId: userId, deviceId: deviceId})
|
{userId, deviceId})
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>,
|
||||||
),
|
|
||||||
hasCancelButton: false,
|
hasCancelButton: false,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
@ -411,6 +456,16 @@ const commands = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Command definitions for autocompletion ONLY:
|
||||||
|
|
||||||
|
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
||||||
|
me: new Command({
|
||||||
|
name: 'me',
|
||||||
|
args: '<message>',
|
||||||
|
description: _td('Displays action'),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
/* eslint-enable babel/no-invalid-this */
|
/* eslint-enable babel/no-invalid-this */
|
||||||
|
@ -421,7 +476,7 @@ const aliases = {
|
||||||
j: "join",
|
j: "join",
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
/**
|
/**
|
||||||
* Process the given text for /commands and perform them.
|
* Process the given text for /commands and perform them.
|
||||||
* @param {string} roomId The room in which the command was performed.
|
* @param {string} roomId The room in which the command was performed.
|
||||||
|
@ -430,11 +485,12 @@ module.exports = {
|
||||||
* processing the command, or 'promise' if a request was sent out.
|
* processing the command, or 'promise' if a request was sent out.
|
||||||
* Returns null if the input didn't match a command.
|
* Returns null if the input didn't match a command.
|
||||||
*/
|
*/
|
||||||
processInput: function(roomId, input) {
|
export function processCommandInput(roomId, input) {
|
||||||
// trim any trailing whitespace, as it can confuse the parser for
|
// trim any trailing whitespace, as it can confuse the parser for
|
||||||
// IRC-style commands
|
// IRC-style commands
|
||||||
input = input.replace(/\s+$/, "");
|
input = input.replace(/\s+$/, '');
|
||||||
if (input[0] === "/") {
|
if (input[0] !== '/') return null; // not a command
|
||||||
|
|
||||||
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
||||||
let cmd;
|
let cmd;
|
||||||
let args;
|
let args;
|
||||||
|
@ -444,27 +500,16 @@ module.exports = {
|
||||||
} else {
|
} else {
|
||||||
cmd = input;
|
cmd = input;
|
||||||
}
|
}
|
||||||
if (cmd === "me") return null;
|
|
||||||
if (aliases[cmd]) {
|
if (aliases[cmd]) {
|
||||||
cmd = aliases[cmd];
|
cmd = aliases[cmd];
|
||||||
}
|
}
|
||||||
if (commands[cmd]) {
|
if (CommandMap[cmd]) {
|
||||||
return commands[cmd].run(roomId, args);
|
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
|
||||||
|
if (!CommandMap[cmd].runFn) return null;
|
||||||
|
|
||||||
|
return CommandMap[cmd].run(roomId, args);
|
||||||
} else {
|
} else {
|
||||||
return reject(_t("Unrecognised command:") + ' ' + input);
|
return reject(_t('Unrecognised command:') + ' ' + input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null; // not a command
|
|
||||||
},
|
|
||||||
|
|
||||||
getCommandList: function() {
|
|
||||||
// Return all the commands plus /me and /markdown which aren't handled like normal commands
|
|
||||||
const cmds = Object.keys(commands).sort().map(function(cmdKey) {
|
|
||||||
return commands[cmdKey];
|
|
||||||
});
|
|
||||||
cmds.push(new Command("me", "<action>", function() {}));
|
|
||||||
cmds.push(new Command("markdown", "<on|off>", function() {}));
|
|
||||||
|
|
||||||
return cmds;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
|
@ -129,6 +129,64 @@ function textForRoomNameEvent(ev) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function textForServerACLEvent(ev) {
|
||||||
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
|
const prevContent = ev.getPrevContent();
|
||||||
|
const changes = [];
|
||||||
|
const current = ev.getContent();
|
||||||
|
const prev = {
|
||||||
|
deny: Array.isArray(prevContent.deny) ? prevContent.deny : [],
|
||||||
|
allow: Array.isArray(prevContent.allow) ? prevContent.allow : [],
|
||||||
|
allow_ip_literals: !(prevContent.allow_ip_literals === false),
|
||||||
|
};
|
||||||
|
let text = "";
|
||||||
|
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
||||||
|
text = `${senderDisplayName} set server ACLs for this room: `;
|
||||||
|
} else {
|
||||||
|
text = `${senderDisplayName} changed the server ACLs for this room: `;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(current.allow)) {
|
||||||
|
current.allow = [];
|
||||||
|
}
|
||||||
|
/* If we know for sure everyone is banned, don't bother showing the diff view */
|
||||||
|
if (current.allow.length === 0) {
|
||||||
|
return text + "🎉 All servers are banned from participating! This room can no longer be used.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(current.deny)) {
|
||||||
|
current.deny = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const bannedServers = current.deny.filter((srv) => typeof(srv) === 'string' && !prev.deny.includes(srv));
|
||||||
|
const unbannedServers = prev.deny.filter((srv) => typeof(srv) === 'string' && !current.deny.includes(srv));
|
||||||
|
const allowedServers = current.allow.filter((srv) => typeof(srv) === 'string' && !prev.allow.includes(srv));
|
||||||
|
const unallowedServers = prev.allow.filter((srv) => typeof(srv) === 'string' && !current.allow.includes(srv));
|
||||||
|
|
||||||
|
if (bannedServers.length > 0) {
|
||||||
|
changes.push(`Servers matching ${bannedServers.join(", ")} are now banned.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unbannedServers.length > 0) {
|
||||||
|
changes.push(`Servers matching ${unbannedServers.join(", ")} were removed from the ban list.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedServers.length > 0) {
|
||||||
|
changes.push(`Servers matching ${allowedServers.join(", ")} are now allowed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unallowedServers.length > 0) {
|
||||||
|
changes.push(`Servers matching ${unallowedServers.join(", ")} were removed from the allowed list.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev.allow_ip_literals !== current.allow_ip_literals) {
|
||||||
|
const allowban = current.allow_ip_literals ? "allowed" : "banned";
|
||||||
|
changes.push(`Participating from a server using an IP literal hostname is now ${allowban}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text + changes.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
function textForMessageEvent(ev) {
|
function textForMessageEvent(ev) {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||||
|
@ -309,6 +367,7 @@ const stateHandlers = {
|
||||||
'm.room.encryption': textForEncryptionEvent,
|
'm.room.encryption': textForEncryptionEvent,
|
||||||
'm.room.power_levels': textForPowerEvent,
|
'm.room.power_levels': textForPowerEvent,
|
||||||
'm.room.pinned_events': textForPinnedEvent,
|
'm.room.pinned_events': textForPinnedEvent,
|
||||||
|
'm.room.server_acl': textForServerACLEvent,
|
||||||
|
|
||||||
'im.vector.modular.widgets': textForWidgetEvent,
|
'im.vector.modular.widgets': textForWidgetEvent,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
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 MatrixClientPeg from './MatrixClientPeg';
|
|
||||||
|
|
||||||
export default class WidgetUtils {
|
|
||||||
/* Returns true if user is able to send state events to modify widgets in this room
|
|
||||||
* (Does not apply to non-room-based / user widgets)
|
|
||||||
* @param roomId -- The ID of the room to check
|
|
||||||
* @return Boolean -- true if the user can modify widgets in this room
|
|
||||||
* @throws Error -- specifies the error reason
|
|
||||||
*/
|
|
||||||
static canUserModifyWidgets(roomId) {
|
|
||||||
if (!roomId) {
|
|
||||||
console.warn('No room ID specified');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
|
||||||
if (!client) {
|
|
||||||
console.warn('User must be be logged in');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = client.getRoom(roomId);
|
|
||||||
if (!room) {
|
|
||||||
console.warn(`Room ID ${roomId} is not recognised`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const me = client.credentials.userId;
|
|
||||||
if (!me) {
|
|
||||||
console.warn('Failed to get user ID');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const member = room.getMember(me);
|
|
||||||
if (!member || member.membership !== "join") {
|
|
||||||
console.warn(`User ${me} is not in room ${roomId}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return room.currentState.maySendStateEvent('im.vector.modular.widgets', me);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -57,14 +57,14 @@ export default class AutocompleteProvider {
|
||||||
|
|
||||||
let match;
|
let match;
|
||||||
while ((match = commandRegex.exec(query)) != null) {
|
while ((match = commandRegex.exec(query)) != null) {
|
||||||
let matchStart = match.index,
|
const start = match.index;
|
||||||
matchEnd = matchStart + match[0].length;
|
const end = start + match[0].length;
|
||||||
if (selection.start <= matchEnd && selection.end >= matchStart) {
|
if (selection.start <= end && selection.end >= start) {
|
||||||
return {
|
return {
|
||||||
command: match,
|
command: match,
|
||||||
range: {
|
range: {
|
||||||
start: matchStart,
|
start,
|
||||||
end: matchEnd,
|
end,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -18,7 +18,9 @@ limitations under the License.
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import type {Component} from 'react';
|
import type {Component} from 'react';
|
||||||
|
import {Room} from 'matrix-js-sdk';
|
||||||
import CommandProvider from './CommandProvider';
|
import CommandProvider from './CommandProvider';
|
||||||
|
import CommunityProvider from './CommunityProvider';
|
||||||
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||||
import RoomProvider from './RoomProvider';
|
import RoomProvider from './RoomProvider';
|
||||||
import UserProvider from './UserProvider';
|
import UserProvider from './UserProvider';
|
||||||
|
@ -48,6 +50,7 @@ const PROVIDERS = [
|
||||||
EmojiProvider,
|
EmojiProvider,
|
||||||
NotifProvider,
|
NotifProvider,
|
||||||
CommandProvider,
|
CommandProvider,
|
||||||
|
CommunityProvider,
|
||||||
DuckDuckGoProvider,
|
DuckDuckGoProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -55,7 +58,7 @@ const PROVIDERS = [
|
||||||
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||||
|
|
||||||
export default class Autocompleter {
|
export default class Autocompleter {
|
||||||
constructor(room) {
|
constructor(room: Room) {
|
||||||
this.room = room;
|
this.room = room;
|
||||||
this.providers = PROVIDERS.map((p) => {
|
this.providers = PROVIDERS.map((p) => {
|
||||||
return new p(room);
|
return new p(room);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 New Vector Ltd
|
||||||
|
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,104 +18,16 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t, _td } from '../languageHandler';
|
import {_t} from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import FuzzyMatcher from './FuzzyMatcher';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
import type {SelectionRange} from './Autocompleter';
|
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||||
|
import {CommandMap} from '../SlashCommands';
|
||||||
|
|
||||||
// TODO merge this with the factory mechanics of SlashCommands?
|
const COMMANDS = Object.values(CommandMap);
|
||||||
// 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',
|
|
||||||
args: '<message>',
|
|
||||||
description: _td('Displays action'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/ban',
|
|
||||||
args: '<user-id> [reason]',
|
|
||||||
description: _td('Bans user with given id'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/unban',
|
|
||||||
args: '<user-id>',
|
|
||||||
description: _td('Unbans user with given id'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/op',
|
|
||||||
args: '<user-id> [<power-level>]',
|
|
||||||
description: _td('Define the power level of a user'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/deop',
|
|
||||||
args: '<user-id>',
|
|
||||||
description: _td('Deops user with given id'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/invite',
|
|
||||||
args: '<user-id>',
|
|
||||||
description: _td('Invites user with given id to current room'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/join',
|
|
||||||
args: '<room-alias>',
|
|
||||||
description: _td('Joins room with given alias'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/part',
|
|
||||||
args: '[<room-alias>]',
|
|
||||||
description: _td('Leave room'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/topic',
|
|
||||||
args: '<topic>',
|
|
||||||
description: _td('Sets the room topic'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/kick',
|
|
||||||
args: '<user-id> [reason]',
|
|
||||||
description: _td('Kicks user with given id'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/nick',
|
|
||||||
args: '<display-name>',
|
|
||||||
description: _td('Changes your display nickname'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/ddg',
|
|
||||||
args: '<query>',
|
|
||||||
description: _td('Searches DuckDuckGo for results'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/tint',
|
|
||||||
args: '<color1> [<color2>]',
|
|
||||||
description: _td('Changes colour scheme of current room'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/verify',
|
|
||||||
args: '<user-id> <device-id> <device-signing-key>',
|
|
||||||
description: _td('Verifies a user, device, and pubkey tuple'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/ignore',
|
|
||||||
args: '<user-id>',
|
|
||||||
description: _td('Ignores a user, hiding their messages from you'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/unignore',
|
|
||||||
args: '<user-id>',
|
|
||||||
description: _td('Stops ignoring a user, showing their messages going forward'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/devtools',
|
|
||||||
args: '',
|
|
||||||
description: _td('Opens the Developer Tools dialog'),
|
|
||||||
},
|
|
||||||
// Omitting `/markdown` as it only seems to apply to OldComposer
|
|
||||||
];
|
|
||||||
|
|
||||||
const COMMAND_RE = /(^\/\w*)/g;
|
const COMMAND_RE = /(^\/\w*)(?: .*)?/g;
|
||||||
|
|
||||||
export default class CommandProvider extends AutocompleteProvider {
|
export default class CommandProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -124,30 +37,37 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange) {
|
async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array<Completion> {
|
||||||
let completions = [];
|
|
||||||
if (!selection.beginning) return completions;
|
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (!command) return [];
|
||||||
let results;
|
|
||||||
if (command[0] == '/') {
|
let matches = [];
|
||||||
results = COMMANDS;
|
// check if the full match differs from the first word (i.e. returns false if the command has args)
|
||||||
} else {
|
if (command[0] !== command[1]) {
|
||||||
results = this.matcher.match(command[0]);
|
// The input looks like a command with arguments, perform exact match
|
||||||
|
const name = command[1].substr(1); // strip leading `/`
|
||||||
|
if (CommandMap[name]) {
|
||||||
|
matches = [CommandMap[name]];
|
||||||
}
|
}
|
||||||
completions = results.map((result) => {
|
} else {
|
||||||
return {
|
if (query === '/') {
|
||||||
completion: result.command + ' ',
|
// If they have just entered `/` show everything
|
||||||
component: (<TextualCompletion
|
matches = COMMANDS;
|
||||||
|
} else {
|
||||||
|
// otherwise fuzzy match against all of the fields
|
||||||
|
matches = this.matcher.match(command[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches.map((result) => ({
|
||||||
|
// If the command is the same as the one they entered, we don't want to discard their arguments
|
||||||
|
completion: result.command === command[1] ? command[0] : (result.command + ' '),
|
||||||
|
component: <TextualCompletion
|
||||||
title={result.command}
|
title={result.command}
|
||||||
subtitle={result.args}
|
subtitle={result.args}
|
||||||
description={_t(result.description)}
|
description={_t(result.description)} />,
|
||||||
/>),
|
|
||||||
range,
|
range,
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
}
|
|
||||||
return completions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
|
111
src/autocomplete/CommunityProvider.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2018 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 React from 'react';
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
|
import {PillCompletion} from './Components';
|
||||||
|
import sdk from '../index';
|
||||||
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
import {makeGroupPermalink} from "../matrix-to";
|
||||||
|
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||||
|
import FlairStore from "../stores/FlairStore";
|
||||||
|
|
||||||
|
const COMMUNITY_REGEX = /\B\+\S*/g;
|
||||||
|
|
||||||
|
function score(query, space) {
|
||||||
|
const index = space.indexOf(query);
|
||||||
|
if (index === -1) {
|
||||||
|
return Infinity;
|
||||||
|
} else {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CommunityProvider extends AutocompleteProvider {
|
||||||
|
constructor() {
|
||||||
|
super(COMMUNITY_REGEX);
|
||||||
|
this.matcher = new FuzzyMatcher([], {
|
||||||
|
keys: ['groupId', 'name', 'shortDescription'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array<Completion> {
|
||||||
|
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
||||||
|
|
||||||
|
// Disable autocompletions when composing commands because of various issues
|
||||||
|
// (see https://github.com/vector-im/riot-web/issues/4762)
|
||||||
|
if (/^(\/join|\/leave)/.test(query)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
let completions = [];
|
||||||
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
|
if (command) {
|
||||||
|
const joinedGroups = cli.getGroups().filter(({myMembership}) => myMembership === 'join');
|
||||||
|
|
||||||
|
const groups = (await Promise.all(joinedGroups.map(async ({groupId}) => {
|
||||||
|
try {
|
||||||
|
return FlairStore.getGroupProfileCached(cli, groupId);
|
||||||
|
} catch (e) { // if FlairStore failed, fall back to just groupId
|
||||||
|
return Promise.resolve({
|
||||||
|
name: '',
|
||||||
|
groupId,
|
||||||
|
avatarUrl: '',
|
||||||
|
shortDescription: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
|
||||||
|
this.matcher.setObjects(groups);
|
||||||
|
|
||||||
|
const matchedString = command[0];
|
||||||
|
completions = this.matcher.match(matchedString);
|
||||||
|
completions = _sortBy(completions, [
|
||||||
|
(c) => score(matchedString, c.groupId),
|
||||||
|
(c) => c.groupId.length,
|
||||||
|
]).map(({avatarUrl, groupId, name}) => ({
|
||||||
|
completion: groupId,
|
||||||
|
suffix: ' ',
|
||||||
|
href: makeGroupPermalink(groupId),
|
||||||
|
component: (
|
||||||
|
<PillCompletion initialComponent={
|
||||||
|
<BaseAvatar name={name || groupId}
|
||||||
|
width={24} height={24}
|
||||||
|
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
|
||||||
|
} title={name} description={groupId} />
|
||||||
|
),
|
||||||
|
range,
|
||||||
|
}))
|
||||||
|
.slice(0, 4);
|
||||||
|
}
|
||||||
|
return completions;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return '💬 ' + _t('Communities');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||||
|
{ completions }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -22,7 +22,7 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import 'whatwg-fetch';
|
import 'whatwg-fetch';
|
||||||
|
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
import type {SelectionRange} from './Autocompleter';
|
import type {SelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
||||||
const REFERRER = 'vector';
|
const REFERRER = 'vector';
|
||||||
|
@ -37,7 +37,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
|
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange) {
|
async getCompletions(query: string, selection: SelectionRange, force?: boolean = false) {
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (!query || !command) {
|
if (!query || !command) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -19,11 +19,11 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {emojioneList, shortnameToImage, shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
|
import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
|
||||||
import FuzzyMatcher from './FuzzyMatcher';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import type {SelectionRange, Completion} from './Autocompleter';
|
import type {Completion, SelectionRange} from './Autocompleter';
|
||||||
import _uniq from 'lodash/uniq';
|
import _uniq from 'lodash/uniq';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
@ -95,7 +95,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange) {
|
async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array<Completion> {
|
||||||
if (SettingsStore.getValue("MessageComposerInput.dontSuggestEmoji")) {
|
if (SettingsStore.getValue("MessageComposerInput.dontSuggestEmoji")) {
|
||||||
return []; // don't give any suggestions if the user doesn't want them
|
return []; // don't give any suggestions if the user doesn't want them
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { _t } from '../languageHandler';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import type {SelectionRange} from './Autocompleter';
|
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const AT_ROOM_REGEX = /@\S*/g;
|
const AT_ROOM_REGEX = /@\S*/g;
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export default class NotifProvider extends AutocompleteProvider {
|
||||||
this.room = room;
|
this.room = room;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange, force = false) {
|
async getCompletions(query: string, selection: SelectionRange, force?:boolean = false): Array<Completion> {
|
||||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
//@flow
|
//@flow
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Aviral Dasgupta
|
Copyright 2017 Aviral Dasgupta
|
||||||
|
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -27,6 +28,10 @@ class KeyMap {
|
||||||
priorityMap = new Map();
|
priorityMap = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripDiacritics(str: string): string {
|
||||||
|
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
export default class QueryMatcher {
|
export default class QueryMatcher {
|
||||||
/**
|
/**
|
||||||
* @param {object[]} objects the objects to perform a match on
|
* @param {object[]} objects the objects to perform a match on
|
||||||
|
@ -46,10 +51,11 @@ export default class QueryMatcher {
|
||||||
objects.forEach((object, i) => {
|
objects.forEach((object, i) => {
|
||||||
const keyValues = _at(object, keys);
|
const keyValues = _at(object, keys);
|
||||||
for (const keyValue of keyValues) {
|
for (const keyValue of keyValues) {
|
||||||
if (!map.hasOwnProperty(keyValue)) {
|
const key = stripDiacritics(keyValue).toLowerCase();
|
||||||
map[keyValue] = [];
|
if (!map.hasOwnProperty(key)) {
|
||||||
|
map[key] = [];
|
||||||
}
|
}
|
||||||
map[keyValue].push(object);
|
map[key].push(object);
|
||||||
}
|
}
|
||||||
keyMap.priorityMap.set(object, i);
|
keyMap.priorityMap.set(object, i);
|
||||||
});
|
});
|
||||||
|
@ -82,7 +88,7 @@ export default class QueryMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
match(query: String): Array<Object> {
|
match(query: String): Array<Object> {
|
||||||
query = query.toLowerCase();
|
query = stripDiacritics(query).toLowerCase();
|
||||||
if (this.options.shouldMatchWordsOnly) {
|
if (this.options.shouldMatchWordsOnly) {
|
||||||
query = query.replace(/[^\w]/g, '');
|
query = query.replace(/[^\w]/g, '');
|
||||||
}
|
}
|
||||||
|
@ -91,7 +97,7 @@ export default class QueryMatcher {
|
||||||
}
|
}
|
||||||
const results = [];
|
const results = [];
|
||||||
this.keyMap.keys.forEach((key) => {
|
this.keyMap.keys.forEach((key) => {
|
||||||
let resultKey = key.toLowerCase();
|
let resultKey = key;
|
||||||
if (this.options.shouldMatchWordsOnly) {
|
if (this.options.shouldMatchWordsOnly) {
|
||||||
resultKey = resultKey.replace(/[^\w]/g, '');
|
resultKey = resultKey.replace(/[^\w]/g, '');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -26,9 +27,9 @@ import {getDisplayAliasForRoom} from '../Rooms';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import {makeRoomPermalink} from "../matrix-to";
|
import {makeRoomPermalink} from "../matrix-to";
|
||||||
import type {SelectionRange} from './Autocompleter';
|
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const ROOM_REGEX = /(?=#)(\S*)/g;
|
const ROOM_REGEX = /\B#\S*/g;
|
||||||
|
|
||||||
function score(query, space) {
|
function score(query, space) {
|
||||||
const index = space.indexOf(query);
|
const index = space.indexOf(query);
|
||||||
|
@ -47,7 +48,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange, force = false) {
|
async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array<Completion> {
|
||||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -23,15 +24,14 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import FuzzyMatcher from './FuzzyMatcher';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import _pull from 'lodash/pull';
|
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
|
|
||||||
import type {Room, RoomMember} from 'matrix-js-sdk';
|
import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk';
|
||||||
import type {SelectionRange} from './Autocompleter';
|
|
||||||
import {makeUserPermalink} from "../matrix-to";
|
import {makeUserPermalink} from "../matrix-to";
|
||||||
|
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const USER_REGEX = /@\S*/g;
|
const USER_REGEX = /\B@\S*/g;
|
||||||
|
|
||||||
// used when you hit 'tab' - we allow some separator chars at the beginning
|
// used when you hit 'tab' - we allow some separator chars at the beginning
|
||||||
// to allow you to tab-complete /mat into /(matthew)
|
// to allow you to tab-complete /mat into /(matthew)
|
||||||
|
@ -47,7 +47,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
this.matcher = new FuzzyMatcher([], {
|
this.matcher = new FuzzyMatcher([], {
|
||||||
keys: ['name', 'userId'],
|
keys: ['name', 'userId'],
|
||||||
shouldMatchPrefix: true,
|
shouldMatchPrefix: true,
|
||||||
shouldMatchWordsOnly: false
|
shouldMatchWordsOnly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._onRoomTimelineBound = this._onRoomTimeline.bind(this);
|
this._onRoomTimelineBound = this._onRoomTimeline.bind(this);
|
||||||
|
@ -64,7 +64,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
_onRoomTimeline(ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: Object) {
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
if (removed) return;
|
if (removed) return;
|
||||||
if (room.roomId !== this.room.roomId) return;
|
if (room.roomId !== this.room.roomId) return;
|
||||||
|
@ -80,7 +80,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
this.onUserSpoke(ev.sender);
|
this.onUserSpoke(ev.sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomStateMember(ev, state, member) {
|
_onRoomStateMember(ev: MatrixEvent, state: RoomState, member: RoomMember) {
|
||||||
// ignore members in other rooms
|
// ignore members in other rooms
|
||||||
if (member.roomId !== this.room.roomId) {
|
if (member.roomId !== this.room.roomId) {
|
||||||
return;
|
return;
|
||||||
|
@ -90,7 +90,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
this.users = null;
|
this.users = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange, force = false) {
|
async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array<Completion> {
|
||||||
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
||||||
|
|
||||||
// lazy-load user list into matcher
|
// lazy-load user list into matcher
|
||||||
|
@ -126,7 +126,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName(): string {
|
||||||
return '👥 ' + _t('Users');
|
return '👥 ' + _t('Users');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,13 +139,9 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
this.users = this.room.getJoinedMembers().filter((member) => {
|
this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId);
|
||||||
if (member.userId !== currentUserId) return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.users = _sortBy(this.users, (member) =>
|
this.users = _sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20);
|
||||||
1E20 - lastSpoken[member.userId] || 1E20,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.matcher.setObjects(this.users);
|
this.matcher.setObjects(this.users);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,12 +16,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
'use strict';
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
const classNames = require('classnames');
|
|
||||||
const React = require('react');
|
|
||||||
const ReactDOM = require('react-dom');
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
|
@ -61,6 +60,54 @@ export default class ContextualMenu extends React.Component {
|
||||||
// If true, insert an invisible screen-sized element behind the
|
// If true, insert an invisible screen-sized element behind the
|
||||||
// menu that when clicked will close it.
|
// menu that when clicked will close it.
|
||||||
hasBackground: PropTypes.bool,
|
hasBackground: PropTypes.bool,
|
||||||
|
|
||||||
|
// The component to render as the context menu
|
||||||
|
elementClass: PropTypes.element.isRequired,
|
||||||
|
// on resize callback
|
||||||
|
windowResize: PropTypes.func,
|
||||||
|
// method to close menu
|
||||||
|
closeMenu: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
contextMenuRect: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.onContextMenu = this.onContextMenu.bind(this);
|
||||||
|
this.collectContextMenuRect = this.collectContextMenuRect.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
collectContextMenuRect(element) {
|
||||||
|
// We don't need to clean up when unmounting, so ignore
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
contextMenuRect: element.getBoundingClientRect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onContextMenu(e) {
|
||||||
|
if (this.props.closeMenu) {
|
||||||
|
this.props.closeMenu();
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const x = e.clientX;
|
||||||
|
const y = e.clientY;
|
||||||
|
|
||||||
|
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
|
||||||
|
// a context menu and its click-guard are up without completely rewriting how the context menus work.
|
||||||
|
setImmediate(() => {
|
||||||
|
const clickEvent = document.createEvent('MouseEvents');
|
||||||
|
clickEvent.initMouseEvent(
|
||||||
|
'contextmenu', true, true, window, 0,
|
||||||
|
0, 0, x, y, false, false,
|
||||||
|
false, false, 0, null,
|
||||||
|
);
|
||||||
|
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -83,6 +130,9 @@ export default class ContextualMenu extends React.Component {
|
||||||
chevronFace = 'right';
|
chevronFace = 'right';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contextMenuRect = this.state.contextMenuRect || null;
|
||||||
|
const padding = 10;
|
||||||
|
|
||||||
const chevronOffset = {};
|
const chevronOffset = {};
|
||||||
if (props.chevronFace) {
|
if (props.chevronFace) {
|
||||||
chevronFace = props.chevronFace;
|
chevronFace = props.chevronFace;
|
||||||
|
@ -90,7 +140,19 @@ export default class ContextualMenu extends React.Component {
|
||||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||||
chevronOffset.left = props.chevronOffset;
|
chevronOffset.left = props.chevronOffset;
|
||||||
} else {
|
} else {
|
||||||
chevronOffset.top = props.chevronOffset;
|
const target = position.top;
|
||||||
|
|
||||||
|
// By default, no adjustment is made
|
||||||
|
let adjusted = target;
|
||||||
|
|
||||||
|
// If we know the dimensions of the context menu, adjust its position
|
||||||
|
// such that it does not leave the (padded) window.
|
||||||
|
if (contextMenuRect) {
|
||||||
|
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
position.top = adjusted;
|
||||||
|
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
|
||||||
}
|
}
|
||||||
|
|
||||||
// To override the default chevron colour, if it's been set
|
// To override the default chevron colour, if it's been set
|
||||||
|
@ -112,7 +174,7 @@ export default class ContextualMenu extends React.Component {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace}></div>;
|
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} />;
|
||||||
const className = 'mx_ContextualMenu_wrapper';
|
const className = 'mx_ContextualMenu_wrapper';
|
||||||
|
|
||||||
const menuClasses = classNames({
|
const menuClasses = classNames({
|
||||||
|
@ -154,17 +216,17 @@ export default class ContextualMenu extends React.Component {
|
||||||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||||
// property set here so you can't close the menu from a button click!
|
// property set here so you can't close the menu from a button click!
|
||||||
return <div className={className} style={position}>
|
return <div className={className} style={position}>
|
||||||
<div className={menuClasses} style={menuStyle}>
|
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect}>
|
||||||
{ chevron }
|
{ chevron }
|
||||||
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
|
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
|
||||||
</div>
|
</div>
|
||||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" onClick={props.closeMenu}></div> }
|
{ props.hasBackground && <div className="mx_ContextualMenu_background" onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||||
<style>{ chevronCSS }</style>
|
<style>{ chevronCSS }</style>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMenu(ElementClass, props) {
|
export function createMenu(ElementClass, props, hasBackground=true) {
|
||||||
const closeMenu = function(...args) {
|
const closeMenu = function(...args) {
|
||||||
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
||||||
|
|
||||||
|
@ -175,8 +237,8 @@ export function createMenu(ElementClass, props) {
|
||||||
|
|
||||||
// We only reference closeMenu once per call to createMenu
|
// We only reference closeMenu once per call to createMenu
|
||||||
const menu = <ContextualMenu
|
const menu = <ContextualMenu
|
||||||
|
hasBackground={hasBackground}
|
||||||
{...props}
|
{...props}
|
||||||
hasBackground={true}
|
|
||||||
elementClass={ElementClass}
|
elementClass={ElementClass}
|
||||||
closeMenu={closeMenu} // eslint-disable-line react/jsx-no-bind
|
closeMenu={closeMenu} // eslint-disable-line react/jsx-no-bind
|
||||||
windowResize={closeMenu} // eslint-disable-line react/jsx-no-bind
|
windowResize={closeMenu} // eslint-disable-line react/jsx-no-bind
|
||||||
|
|
|
@ -68,8 +68,8 @@ const FilePanel = React.createClass({
|
||||||
"room": {
|
"room": {
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"contains_url": true,
|
"contains_url": true,
|
||||||
"not_types": [
|
"types": [
|
||||||
"m.sticker",
|
"m.room.message",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -432,11 +432,14 @@ export default React.createClass({
|
||||||
|
|
||||||
this._changeAvatarComponent = null;
|
this._changeAvatarComponent = null;
|
||||||
this._initGroupStore(this.props.groupId, true);
|
this._initGroupStore(this.props.groupId, true);
|
||||||
|
|
||||||
|
this._dispatcherRef = dis.register(this._onAction);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
this._unmounted = true;
|
this._unmounted = true;
|
||||||
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
dis.unregister(this._dispatcherRef);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
@ -559,16 +562,33 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onShareClick: function() {
|
||||||
|
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||||
|
Modal.createTrackedDialog('share community dialog', '', ShareDialog, {
|
||||||
|
target: this._matrixClient.getGroup(this.props.groupId),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_onCancelClick: function() {
|
_onCancelClick: function() {
|
||||||
this._closeSettings();
|
this._closeSettings();
|
||||||
},
|
},
|
||||||
|
|
||||||
_closeSettings() {
|
_onAction(payload) {
|
||||||
|
switch (payload.action) {
|
||||||
|
// NOTE: close_settings is an app-wide dispatch; as it is dispatched from MatrixChat
|
||||||
|
case 'close_settings':
|
||||||
this.setState({
|
this.setState({
|
||||||
editing: false,
|
editing: false,
|
||||||
profileForm: null,
|
profileForm: null,
|
||||||
});
|
});
|
||||||
dis.dispatch({action: 'panel_disable'});
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_closeSettings() {
|
||||||
|
dis.dispatch({action: 'close_settings'});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onNameChange: function(value) {
|
_onNameChange: function(value) {
|
||||||
|
@ -1039,7 +1059,7 @@ export default React.createClass({
|
||||||
<input type="radio"
|
<input type="radio"
|
||||||
value={GROUP_JOINPOLICY_INVITE}
|
value={GROUP_JOINPOLICY_INVITE}
|
||||||
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_INVITE}
|
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_INVITE}
|
||||||
onClick={this._onJoinableChange}
|
onChange={this._onJoinableChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_GroupView_label_text">
|
<div className="mx_GroupView_label_text">
|
||||||
{ _t('Only people who have been invited') }
|
{ _t('Only people who have been invited') }
|
||||||
|
@ -1051,7 +1071,7 @@ export default React.createClass({
|
||||||
<input type="radio"
|
<input type="radio"
|
||||||
value={GROUP_JOINPOLICY_OPEN}
|
value={GROUP_JOINPOLICY_OPEN}
|
||||||
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_OPEN}
|
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_OPEN}
|
||||||
onClick={this._onJoinableChange}
|
onChange={this._onJoinableChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_GroupView_label_text">
|
<div className="mx_GroupView_label_text">
|
||||||
{ _t('Everyone') }
|
{ _t('Everyone') }
|
||||||
|
@ -1114,10 +1134,6 @@ export default React.createClass({
|
||||||
let avatarNode;
|
let avatarNode;
|
||||||
let nameNode;
|
let nameNode;
|
||||||
let shortDescNode;
|
let shortDescNode;
|
||||||
const bodyNodes = [
|
|
||||||
this._getMembershipSection(),
|
|
||||||
this._getGroupSection(),
|
|
||||||
];
|
|
||||||
const rightButtons = [];
|
const rightButtons = [];
|
||||||
if (this.state.editing && this.state.isUserPrivileged) {
|
if (this.state.editing && this.state.isUserPrivileged) {
|
||||||
let avatarImage;
|
let avatarImage;
|
||||||
|
@ -1194,6 +1210,7 @@ export default React.createClass({
|
||||||
shortDescNode = <span onClick={onGroupHeaderItemClick}>{ summary.profile.short_description }</span>;
|
shortDescNode = <span onClick={onGroupHeaderItemClick}>{ summary.profile.short_description }</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
rightButtons.push(
|
rightButtons.push(
|
||||||
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||||
|
@ -1218,6 +1235,11 @@ export default React.createClass({
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
rightButtons.push(
|
||||||
|
<AccessibleButton className="mx_GroupHeader_button" onClick={this._onShareClick} title={_t('Share Community')} key="_shareButton">
|
||||||
|
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
|
||||||
|
</AccessibleButton>,
|
||||||
|
);
|
||||||
if (this.props.collapsedRhs) {
|
if (this.props.collapsedRhs) {
|
||||||
rightButtons.push(
|
rightButtons.push(
|
||||||
<AccessibleButton className="mx_GroupHeader_button"
|
<AccessibleButton className="mx_GroupHeader_button"
|
||||||
|
@ -1256,7 +1278,8 @@ export default React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||||
{ bodyNodes }
|
{ this._getMembershipSection() }
|
||||||
|
{ this._getGroupSection() }
|
||||||
</GeminiScrollbarWrapper>
|
</GeminiScrollbarWrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -82,17 +82,26 @@ var LeftPanel = React.createClass({
|
||||||
|
|
||||||
_onKeyDown: function(ev) {
|
_onKeyDown: function(ev) {
|
||||||
if (!this.focusedElement) return;
|
if (!this.focusedElement) return;
|
||||||
let handled = false;
|
let handled = true;
|
||||||
|
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
|
case KeyCode.TAB:
|
||||||
|
this._onMoveFocus(ev.shiftKey);
|
||||||
|
break;
|
||||||
case KeyCode.UP:
|
case KeyCode.UP:
|
||||||
this._onMoveFocus(true);
|
this._onMoveFocus(true);
|
||||||
handled = true;
|
|
||||||
break;
|
break;
|
||||||
case KeyCode.DOWN:
|
case KeyCode.DOWN:
|
||||||
this._onMoveFocus(false);
|
this._onMoveFocus(false);
|
||||||
handled = true;
|
|
||||||
break;
|
break;
|
||||||
|
case KeyCode.ENTER:
|
||||||
|
this._onMoveFocus(false);
|
||||||
|
if (this.focusedElement) {
|
||||||
|
this.focusedElement.click();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
handled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handled) {
|
if (handled) {
|
||||||
|
@ -102,37 +111,33 @@ var LeftPanel = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onMoveFocus: function(up) {
|
_onMoveFocus: function(up) {
|
||||||
var element = this.focusedElement;
|
let element = this.focusedElement;
|
||||||
|
|
||||||
// unclear why this isn't needed
|
// unclear why this isn't needed
|
||||||
// var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
|
// var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
|
||||||
// this.focusDirection = up;
|
// this.focusDirection = up;
|
||||||
|
|
||||||
var descending = false; // are we currently descending or ascending through the DOM tree?
|
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||||
var classes;
|
let classes;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
var child = up ? element.lastElementChild : element.firstElementChild;
|
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||||
var sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||||
|
|
||||||
if (descending) {
|
if (descending) {
|
||||||
if (child) {
|
if (child) {
|
||||||
element = child;
|
element = child;
|
||||||
}
|
} else if (sibling) {
|
||||||
else if (sibling) {
|
|
||||||
element = sibling;
|
element = sibling;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
descending = false;
|
descending = false;
|
||||||
element = element.parentElement;
|
element = element.parentElement;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (sibling) {
|
if (sibling) {
|
||||||
element = sibling;
|
element = sibling;
|
||||||
descending = true;
|
descending = true;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
element = element.parentElement;
|
element = element.parentElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,7 +149,6 @@ var LeftPanel = React.createClass({
|
||||||
descending = true;
|
descending = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} while (element && !(
|
} while (element && !(
|
||||||
classes.contains("mx_RoomTile") ||
|
classes.contains("mx_RoomTile") ||
|
||||||
classes.contains("mx_SearchBox_search") ||
|
classes.contains("mx_SearchBox_search") ||
|
||||||
|
|
|
@ -255,6 +255,22 @@ const LoggedInView = React.createClass({
|
||||||
), true);
|
), true);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onClick: function(ev) {
|
||||||
|
// When the panels are disabled, clicking on them results in a mouse event
|
||||||
|
// which bubbles to certain elements in the tree. When this happens, close
|
||||||
|
// any settings page that is currently open (user/room/group).
|
||||||
|
if (this.props.leftDisabled &&
|
||||||
|
this.props.rightDisabled &&
|
||||||
|
(
|
||||||
|
ev.target.className === 'mx_MatrixChat' ||
|
||||||
|
ev.target.className === 'mx_MatrixChat_middlePanel' ||
|
||||||
|
ev.target.className === 'mx_RoomView'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
dis.dispatch({ action: 'close_settings' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||||
|
@ -295,7 +311,7 @@ const LoggedInView = React.createClass({
|
||||||
|
|
||||||
case PageTypes.UserSettings:
|
case PageTypes.UserSettings:
|
||||||
page_element = <UserSettings
|
page_element = <UserSettings
|
||||||
onClose={this.props.onUserSettingsClose}
|
onClose={this.props.onCloseAllSettings}
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||||
teamToken={this.props.teamToken}
|
teamToken={this.props.teamToken}
|
||||||
|
@ -380,7 +396,7 @@ const LoggedInView = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers}>
|
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onClick={this._onClick}>
|
||||||
{ topBar }
|
{ topBar }
|
||||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||||
<div className={bodyClasses}>
|
<div className={bodyClasses}>
|
||||||
|
|
|
@ -23,6 +23,7 @@ import PropTypes from 'prop-types';
|
||||||
import Matrix from "matrix-js-sdk";
|
import Matrix from "matrix-js-sdk";
|
||||||
|
|
||||||
import Analytics from "../../Analytics";
|
import Analytics from "../../Analytics";
|
||||||
|
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||||
import PlatformPeg from "../../PlatformPeg";
|
import PlatformPeg from "../../PlatformPeg";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
|
@ -398,6 +399,9 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
startPageChangeTimer() {
|
startPageChangeTimer() {
|
||||||
|
// Tor doesn't support performance
|
||||||
|
if (!performance || !performance.mark) return null;
|
||||||
|
|
||||||
// This shouldn't happen because componentWillUpdate and componentDidUpdate
|
// This shouldn't happen because componentWillUpdate and componentDidUpdate
|
||||||
// are used.
|
// are used.
|
||||||
if (this._pageChanging) {
|
if (this._pageChanging) {
|
||||||
|
@ -409,6 +413,9 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
stopPageChangeTimer() {
|
stopPageChangeTimer() {
|
||||||
|
// Tor doesn't support performance
|
||||||
|
if (!performance || !performance.mark) return null;
|
||||||
|
|
||||||
if (!this._pageChanging) {
|
if (!this._pageChanging) {
|
||||||
console.warn('MatrixChat.stopPageChangeTimer: timer not started');
|
console.warn('MatrixChat.stopPageChangeTimer: timer not started');
|
||||||
return;
|
return;
|
||||||
|
@ -560,6 +567,27 @@ export default React.createClass({
|
||||||
this._setPage(PageTypes.UserSettings);
|
this._setPage(PageTypes.UserSettings);
|
||||||
this.notifyNewScreen('settings');
|
this.notifyNewScreen('settings');
|
||||||
break;
|
break;
|
||||||
|
case 'close_settings':
|
||||||
|
this.setState({
|
||||||
|
leftDisabled: false,
|
||||||
|
rightDisabled: false,
|
||||||
|
middleDisabled: false,
|
||||||
|
});
|
||||||
|
if (this.state.page_type === PageTypes.UserSettings) {
|
||||||
|
// We do this to get setPage and notifyNewScreen
|
||||||
|
if (this.state.currentRoomId) {
|
||||||
|
this._viewRoom({
|
||||||
|
room_id: this.state.currentRoomId,
|
||||||
|
});
|
||||||
|
} else if (this.state.currentGroupId) {
|
||||||
|
this._viewGroup({
|
||||||
|
group_id: this.state.currentGroupId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._viewHome();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'view_create_room':
|
case 'view_create_room':
|
||||||
this._createRoom();
|
this._createRoom();
|
||||||
break;
|
break;
|
||||||
|
@ -577,19 +605,10 @@ export default React.createClass({
|
||||||
this.notifyNewScreen('groups');
|
this.notifyNewScreen('groups');
|
||||||
break;
|
break;
|
||||||
case 'view_group':
|
case 'view_group':
|
||||||
{
|
this._viewGroup(payload);
|
||||||
const groupId = payload.group_id;
|
|
||||||
this.setState({
|
|
||||||
currentGroupId: groupId,
|
|
||||||
currentGroupIsNew: payload.group_is_new,
|
|
||||||
});
|
|
||||||
this._setPage(PageTypes.GroupView);
|
|
||||||
this.notifyNewScreen('group/' + groupId);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'view_home_page':
|
case 'view_home_page':
|
||||||
this._setPage(PageTypes.HomePage);
|
this._viewHome();
|
||||||
this.notifyNewScreen('home');
|
|
||||||
break;
|
break;
|
||||||
case 'view_set_mxid':
|
case 'view_set_mxid':
|
||||||
this._setMxId(payload);
|
this._setMxId(payload);
|
||||||
|
@ -632,7 +651,8 @@ export default React.createClass({
|
||||||
middleDisabled: payload.middleDisabled || false,
|
middleDisabled: payload.middleDisabled || false,
|
||||||
rightDisabled: payload.rightDisabled || payload.sideDisabled || false,
|
rightDisabled: payload.rightDisabled || payload.sideDisabled || false,
|
||||||
});
|
});
|
||||||
break; }
|
break;
|
||||||
|
}
|
||||||
case 'set_theme':
|
case 'set_theme':
|
||||||
this._onSetTheme(payload.value);
|
this._onSetTheme(payload.value);
|
||||||
break;
|
break;
|
||||||
|
@ -781,7 +801,6 @@ export default React.createClass({
|
||||||
// @param {string=} roomInfo.room_id ID of the room to join. One of room_id or room_alias must be given.
|
// @param {string=} roomInfo.room_id ID of the room to join. One of room_id or room_alias must be given.
|
||||||
// @param {string=} roomInfo.room_alias Alias of the room to join. One of room_id or room_alias must be given.
|
// @param {string=} roomInfo.room_alias Alias of the room to join. One of room_id or room_alias must be given.
|
||||||
// @param {boolean=} roomInfo.auto_join If true, automatically attempt to join the room if not already a member.
|
// @param {boolean=} roomInfo.auto_join If true, automatically attempt to join the room if not already a member.
|
||||||
// @param {boolean=} roomInfo.show_settings Makes RoomView show the room settings dialog.
|
|
||||||
// @param {string=} roomInfo.event_id ID of the event in this room to show: this will cause a switch to the
|
// @param {string=} roomInfo.event_id ID of the event in this room to show: this will cause a switch to the
|
||||||
// context of that particular event.
|
// context of that particular event.
|
||||||
// @param {boolean=} roomInfo.highlighted If true, add event_id to the hash of the URL
|
// @param {boolean=} roomInfo.highlighted If true, add event_id to the hash of the URL
|
||||||
|
@ -848,6 +867,21 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_viewGroup: function(payload) {
|
||||||
|
const groupId = payload.group_id;
|
||||||
|
this.setState({
|
||||||
|
currentGroupId: groupId,
|
||||||
|
currentGroupIsNew: payload.group_is_new,
|
||||||
|
});
|
||||||
|
this._setPage(PageTypes.GroupView);
|
||||||
|
this.notifyNewScreen('group/' + groupId);
|
||||||
|
},
|
||||||
|
|
||||||
|
_viewHome: function() {
|
||||||
|
this._setPage(PageTypes.HomePage);
|
||||||
|
this.notifyNewScreen('home');
|
||||||
|
},
|
||||||
|
|
||||||
_setMxId: function(payload) {
|
_setMxId: function(payload) {
|
||||||
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||||
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
||||||
|
@ -957,6 +991,7 @@ export default React.createClass({
|
||||||
if (rule !== "public") {
|
if (rule !== "public") {
|
||||||
warnings.push((
|
warnings.push((
|
||||||
<span className="warning" key="non_public_warning">
|
<span className="warning" key="non_public_warning">
|
||||||
|
{' '/* Whitespace, otherwise the sentences get smashed together */ }
|
||||||
{ _t("This room is not public. You will not be able to rejoin without an invite.") }
|
{ _t("This room is not public. You will not be able to rejoin without an invite.") }
|
||||||
</span>
|
</span>
|
||||||
));
|
));
|
||||||
|
@ -995,10 +1030,20 @@ export default React.createClass({
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
modal.close();
|
modal.close();
|
||||||
console.error("Failed to leave room " + roomId + " " + err);
|
console.error("Failed to leave room " + roomId + " " + err);
|
||||||
|
let title = _t("Failed to leave room");
|
||||||
|
let message = _t("Server may be unavailable, overloaded, or you hit a bug.");
|
||||||
|
if (err.errcode == 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') {
|
||||||
|
title = _t("Can't leave Server Notices room");
|
||||||
|
message = _t(
|
||||||
|
"This room is used for important messages from the Homeserver, " +
|
||||||
|
"so you cannot leave it.",
|
||||||
|
);
|
||||||
|
} else if (err && err.message) {
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
|
||||||
title: _t("Failed to leave room"),
|
title: title,
|
||||||
description: (err && err.message ? err.message :
|
description: message,
|
||||||
_t("Server may be unavailable, overloaded, or you hit a bug.")),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1099,11 +1144,6 @@ export default React.createClass({
|
||||||
} else if (this._is_registered) {
|
} else if (this._is_registered) {
|
||||||
this._is_registered = false;
|
this._is_registered = false;
|
||||||
|
|
||||||
// Set the display name = user ID localpart
|
|
||||||
MatrixClientPeg.get().setDisplayName(
|
|
||||||
MatrixClientPeg.get().getUserIdLocalpart(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||||
createRoom({
|
createRoom({
|
||||||
dmUserId: this.props.config.welcomeUserId,
|
dmUserId: this.props.config.welcomeUserId,
|
||||||
|
@ -1231,6 +1271,28 @@ export default React.createClass({
|
||||||
action: 'logout',
|
action: 'logout',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
cli.on('no_consent', function(message, consentUri) {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('No Consent Dialog', '', QuestionDialog, {
|
||||||
|
title: _t('Terms and Conditions'),
|
||||||
|
description: <div>
|
||||||
|
<p> { _t(
|
||||||
|
'To continue using the %(homeserverDomain)s homeserver ' +
|
||||||
|
'you must review and agree to our terms and conditions.',
|
||||||
|
{ homeserverDomain: cli.getDomain() },
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
</div>,
|
||||||
|
button: _t('Review terms and conditions'),
|
||||||
|
cancelButton: _t('Dismiss'),
|
||||||
|
onFinished: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
window.open(consentUri, '_blank');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, null, true);
|
||||||
|
});
|
||||||
|
|
||||||
cli.on("accountData", function(ev) {
|
cli.on("accountData", function(ev) {
|
||||||
if (ev.getType() === 'im.vector.web.settings') {
|
if (ev.getType() === 'im.vector.web.settings') {
|
||||||
if (ev.getContent() && ev.getContent().theme) {
|
if (ev.getContent() && ev.getContent().theme) {
|
||||||
|
@ -1242,6 +1304,32 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dft = new DecryptionFailureTracker((total, errorCode) => {
|
||||||
|
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
|
||||||
|
}, (errorCode) => {
|
||||||
|
// Map JS-SDK error codes to tracker codes for aggregation
|
||||||
|
switch (errorCode) {
|
||||||
|
case 'MEGOLM_UNKNOWN_INBOUND_SESSION_ID':
|
||||||
|
return 'olm_keys_not_sent_error';
|
||||||
|
case 'OLM_UNKNOWN_MESSAGE_INDEX':
|
||||||
|
return 'olm_index_error';
|
||||||
|
case undefined:
|
||||||
|
return 'unexpected_error';
|
||||||
|
default:
|
||||||
|
return 'unspecified_error';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shelved for later date when we have time to think about persisting history of
|
||||||
|
// tracked events across sessions.
|
||||||
|
// dft.loadTrackedEventHashMap();
|
||||||
|
|
||||||
|
dft.start();
|
||||||
|
|
||||||
|
// When logging out, stop tracking failures and destroy state
|
||||||
|
cli.on("Session.logged_out", () => dft.stop());
|
||||||
|
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
|
||||||
|
|
||||||
const krh = new KeyRequestHandler(cli);
|
const krh = new KeyRequestHandler(cli);
|
||||||
cli.on("crypto.roomKeyRequest", (req) => {
|
cli.on("crypto.roomKeyRequest", (req) => {
|
||||||
krh.handleKeyRequest(req);
|
krh.handleKeyRequest(req);
|
||||||
|
@ -1573,19 +1661,8 @@ export default React.createClass({
|
||||||
this._setPageSubtitle(subtitle);
|
this._setPageSubtitle(subtitle);
|
||||||
},
|
},
|
||||||
|
|
||||||
onUserSettingsClose: function() {
|
onCloseAllSettings() {
|
||||||
// XXX: use browser history instead to find the previous room?
|
dis.dispatch({ action: 'close_settings' });
|
||||||
// or maintain a this.state.pageHistory in _setPage()?
|
|
||||||
if (this.state.currentRoomId) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: this.state.currentRoomId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_home_page',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onServerConfigChange(config) {
|
onServerConfigChange(config) {
|
||||||
|
@ -1644,7 +1721,7 @@ export default React.createClass({
|
||||||
return (
|
return (
|
||||||
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
|
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
|
||||||
onRoomCreated={this.onRoomCreated}
|
onRoomCreated={this.onRoomCreated}
|
||||||
onUserSettingsClose={this.onUserSettingsClose}
|
onCloseAllSettings={this.onCloseAllSettings}
|
||||||
onRegistered={this.onRegistered}
|
onRegistered={this.onRegistered}
|
||||||
currentRoomId={this.state.currentRoomId}
|
currentRoomId={this.state.currentRoomId}
|
||||||
teamToken={this._teamToken}
|
teamToken={this._teamToken}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -25,6 +26,9 @@ import sdk from '../../index';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
|
|
||||||
|
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
|
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||||
|
|
||||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||||
*/
|
*/
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -189,7 +193,7 @@ module.exports = React.createClass({
|
||||||
/**
|
/**
|
||||||
* Page up/down.
|
* Page up/down.
|
||||||
*
|
*
|
||||||
* mult: -1 to page up, +1 to page down
|
* @param {number} mult: -1 to page up, +1 to page down
|
||||||
*/
|
*/
|
||||||
scrollRelative: function(mult) {
|
scrollRelative: function(mult) {
|
||||||
if (this.refs.scrollPanel) {
|
if (this.refs.scrollPanel) {
|
||||||
|
@ -199,6 +203,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll up/down in response to a scroll key
|
* Scroll up/down in response to a scroll key
|
||||||
|
*
|
||||||
|
* @param {KeyboardEvent} ev: the keyboard event to handle
|
||||||
*/
|
*/
|
||||||
handleScrollKey: function(ev) {
|
handleScrollKey: function(ev) {
|
||||||
if (this.refs.scrollPanel) {
|
if (this.refs.scrollPanel) {
|
||||||
|
@ -257,6 +263,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
this.eventNodes = {};
|
this.eventNodes = {};
|
||||||
|
|
||||||
|
let visible = false;
|
||||||
let i;
|
let i;
|
||||||
|
|
||||||
// first figure out which is the last event in the list which we're
|
// first figure out which is the last event in the list which we're
|
||||||
|
@ -297,7 +304,7 @@ module.exports = React.createClass({
|
||||||
// if the readmarker has moved, cancel any active ghost.
|
// if the readmarker has moved, cancel any active ghost.
|
||||||
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
|
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
|
||||||
this.props.readMarkerVisible &&
|
this.props.readMarkerVisible &&
|
||||||
this.currentReadMarkerEventId != this.props.readMarkerEventId) {
|
this.currentReadMarkerEventId !== this.props.readMarkerEventId) {
|
||||||
this.currentGhostEventId = null;
|
this.currentGhostEventId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,8 +411,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
let isVisibleReadMarker = false;
|
let isVisibleReadMarker = false;
|
||||||
|
|
||||||
if (eventId == this.props.readMarkerEventId) {
|
if (eventId === this.props.readMarkerEventId) {
|
||||||
var visible = this.props.readMarkerVisible;
|
visible = this.props.readMarkerVisible;
|
||||||
|
|
||||||
// if the read marker comes at the end of the timeline (except
|
// if the read marker comes at the end of the timeline (except
|
||||||
// for local echoes, which are excluded from RMs, because they
|
// for local echoes, which are excluded from RMs, because they
|
||||||
|
@ -423,11 +430,11 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// XXX: there should be no need for a ghost tile - we should just use a
|
// XXX: there should be no need for a ghost tile - we should just use a
|
||||||
// a dispatch (user_activity_end) to start the RM animation.
|
// a dispatch (user_activity_end) to start the RM animation.
|
||||||
if (eventId == this.currentGhostEventId) {
|
if (eventId === this.currentGhostEventId) {
|
||||||
// if we're showing an animation, continue to show it.
|
// if we're showing an animation, continue to show it.
|
||||||
ret.push(this._getReadMarkerGhostTile());
|
ret.push(this._getReadMarkerGhostTile());
|
||||||
} else if (!isVisibleReadMarker &&
|
} else if (!isVisibleReadMarker &&
|
||||||
eventId == this.currentReadMarkerEventId) {
|
eventId === this.currentReadMarkerEventId) {
|
||||||
// there is currently a read-up-to marker at this point, but no
|
// there is currently a read-up-to marker at this point, but no
|
||||||
// more. Show an animation of it disappearing.
|
// more. Show an animation of it disappearing.
|
||||||
ret.push(this._getReadMarkerGhostTile());
|
ret.push(this._getReadMarkerGhostTile());
|
||||||
|
@ -449,16 +456,17 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// Some events should appear as continuations from previous events of
|
// Some events should appear as continuations from previous events of
|
||||||
// different types.
|
// different types.
|
||||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
|
||||||
const eventTypeContinues =
|
const eventTypeContinues =
|
||||||
prevEvent !== null &&
|
prevEvent !== null &&
|
||||||
continuedTypes.includes(mxEv.getType()) &&
|
continuedTypes.includes(mxEv.getType()) &&
|
||||||
continuedTypes.includes(prevEvent.getType());
|
continuedTypes.includes(prevEvent.getType());
|
||||||
|
|
||||||
if (prevEvent !== null
|
// if there is a previous event and it has the same sender as this event
|
||||||
&& prevEvent.sender && mxEv.sender
|
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId &&
|
||||||
&& (mxEv.getType() == prevEvent.getType() || eventTypeContinues)) {
|
(mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
|
||||||
|
(mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) {
|
||||||
continuation = true;
|
continuation = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,7 +501,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventId = mxEv.getId();
|
const eventId = mxEv.getId();
|
||||||
const highlight = (eventId == this.props.highlightedEventId);
|
const highlight = (eventId === this.props.highlightedEventId);
|
||||||
|
|
||||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||||
// Local echos have a send "status".
|
// Local echos have a send "status".
|
||||||
|
@ -632,7 +640,8 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
let topSpinner, bottomSpinner;
|
let topSpinner;
|
||||||
|
let bottomSpinner;
|
||||||
if (this.props.backPaginating) {
|
if (this.props.backPaginating) {
|
||||||
topSpinner = <li key="_topSpinner"><Spinner /></li>;
|
topSpinner = <li key="_topSpinner"><Spinner /></li>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default withMatrixClient(React.createClass({
|
||||||
if (this.state.groups) {
|
if (this.state.groups) {
|
||||||
const groupNodes = [];
|
const groupNodes = [];
|
||||||
this.state.groups.forEach((g) => {
|
this.state.groups.forEach((g) => {
|
||||||
groupNodes.push(<GroupTile groupId={g} />);
|
groupNodes.push(<GroupTile key={g} groupId={g} />);
|
||||||
});
|
});
|
||||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||||
content = groupNodes.length > 0 ?
|
content = groupNodes.length > 0 ?
|
||||||
|
@ -124,7 +124,7 @@ export default withMatrixClient(React.createClass({
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
{/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
||||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
|
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
|
||||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -140,7 +140,7 @@ export default withMatrixClient(React.createClass({
|
||||||
{ 'i': (sub) => <i>{ sub }</i> })
|
{ 'i': (sub) => <i>{ sub }</i> })
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>*/}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MyGroups_content">
|
<div className="mx_MyGroups_content">
|
||||||
{ contentHeader }
|
{ contentHeader }
|
||||||
|
|
|
@ -25,6 +25,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||||
import Resend from '../../Resend';
|
import Resend from '../../Resend';
|
||||||
import * as cryptodevices from '../../cryptodevices';
|
import * as cryptodevices from '../../cryptodevices';
|
||||||
|
import dis from '../../dispatcher';
|
||||||
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
|
@ -157,10 +158,12 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_onResendAllClick: function() {
|
_onResendAllClick: function() {
|
||||||
Resend.resendUnsentEvents(this.props.room);
|
Resend.resendUnsentEvents(this.props.room);
|
||||||
|
dis.dispatch({action: 'focus_composer'});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onCancelAllClick: function() {
|
_onCancelAllClick: function() {
|
||||||
Resend.cancelUnsentEvents(this.props.room);
|
Resend.cancelUnsentEvents(this.props.room);
|
||||||
|
dis.dispatch({action: 'focus_composer'});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onShowDevicesClick: function() {
|
_onShowDevicesClick: function() {
|
||||||
|
@ -305,7 +308,26 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (
|
let consentError = null;
|
||||||
|
for (const m of unsentMessages) {
|
||||||
|
if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') {
|
||||||
|
consentError = m.error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (consentError) {
|
||||||
|
title = _t(
|
||||||
|
"You can't send any messages until you review and agree to " +
|
||||||
|
"<consentLink>our terms and conditions</consentLink>.",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'consentLink': (sub) =>
|
||||||
|
<a href={consentError.data && consentError.data.consent_uri} target="_blank">
|
||||||
|
{ sub }
|
||||||
|
</a>,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
unsentMessages.length === 1 &&
|
unsentMessages.length === 1 &&
|
||||||
unsentMessages[0].error &&
|
unsentMessages[0].error &&
|
||||||
unsentMessages[0].error.data &&
|
unsentMessages[0].error.data &&
|
||||||
|
@ -329,12 +351,14 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt={_t("Warning")} />
|
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt={_t("Warning")} />
|
||||||
|
<div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
{ title }
|
{ title }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
{ content }
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -350,6 +374,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ " />
|
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ " />
|
||||||
|
<div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
{ _t('Connectivity to the server has been lost.') }
|
{ _t('Connectivity to the server has been lost.') }
|
||||||
</div>
|
</div>
|
||||||
|
@ -357,6 +382,7 @@ module.exports = React.createClass({
|
||||||
{ _t('Sent messages will be stored until your connection has returned.') }
|
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,56 +16,53 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
var React = require('react');
|
import sdk from '../../index';
|
||||||
var ReactDOM = require('react-dom');
|
|
||||||
var classNames = require('classnames');
|
|
||||||
var sdk = require('../../index');
|
|
||||||
import { Droppable } from 'react-beautiful-dnd';
|
import { Droppable } from 'react-beautiful-dnd';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
var dis = require('../../dispatcher');
|
import dis from '../../dispatcher';
|
||||||
var Unread = require('../../Unread');
|
import Unread from '../../Unread';
|
||||||
var MatrixClientPeg = require('../../MatrixClientPeg');
|
import * as RoomNotifs from '../../RoomNotifs';
|
||||||
var RoomNotifs = require('../../RoomNotifs');
|
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||||
var FormattingUtils = require('../../utils/FormattingUtils');
|
|
||||||
var AccessibleButton = require('../../components/views/elements/AccessibleButton');
|
|
||||||
import Modal from '../../Modal';
|
|
||||||
import { KeyCode } from '../../Keyboard';
|
import { KeyCode } from '../../Keyboard';
|
||||||
|
import { Group } from 'matrix-js-sdk';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
|
||||||
// turn this on for drop & drag console debugging galore
|
// turn this on for drop & drag console debugging galore
|
||||||
var debug = false;
|
const debug = false;
|
||||||
|
|
||||||
const TRUNCATE_AT = 10;
|
const TRUNCATE_AT = 10;
|
||||||
|
|
||||||
var RoomSubList = React.createClass({
|
const RoomSubList = React.createClass({
|
||||||
displayName: 'RoomSubList',
|
displayName: 'RoomSubList',
|
||||||
|
|
||||||
debug: debug,
|
debug: debug,
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
label: React.PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
tagName: React.PropTypes.string,
|
tagName: PropTypes.string,
|
||||||
editable: React.PropTypes.bool,
|
editable: PropTypes.bool,
|
||||||
|
|
||||||
order: React.PropTypes.string.isRequired,
|
order: PropTypes.string.isRequired,
|
||||||
|
|
||||||
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
||||||
isInvite: React.PropTypes.bool,
|
isInvite: PropTypes.bool,
|
||||||
|
|
||||||
startAsHidden: React.PropTypes.bool,
|
startAsHidden: PropTypes.bool,
|
||||||
showSpinner: React.PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
||||||
collapsed: React.PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||||
onHeaderClick: React.PropTypes.func,
|
onHeaderClick: PropTypes.func,
|
||||||
alwaysShowHeader: React.PropTypes.bool,
|
alwaysShowHeader: PropTypes.bool,
|
||||||
incomingCall: React.PropTypes.object,
|
incomingCall: PropTypes.object,
|
||||||
onShowMoreRooms: React.PropTypes.func,
|
onShowMoreRooms: PropTypes.func,
|
||||||
searchFilter: React.PropTypes.string,
|
searchFilter: PropTypes.string,
|
||||||
emptyContent: React.PropTypes.node, // content shown if the list is empty
|
emptyContent: PropTypes.node, // content shown if the list is empty
|
||||||
headerItems: React.PropTypes.node, // content shown in the sublist header
|
headerItems: PropTypes.node, // content shown in the sublist header
|
||||||
extraTiles: React.PropTypes.arrayOf(React.PropTypes.node), // extra elements added beneath tiles
|
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
||||||
|
showEmpty: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -77,10 +75,13 @@ var RoomSubList = React.createClass({
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
onHeaderClick: function() {}, // NOP
|
onHeaderClick: function() {
|
||||||
onShowMoreRooms: function() {}, // NOP
|
}, // NOP
|
||||||
|
onShowMoreRooms: function() {
|
||||||
|
}, // NOP
|
||||||
extraTiles: [],
|
extraTiles: [],
|
||||||
isInvite: false,
|
isInvite: false,
|
||||||
|
showEmpty: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -105,15 +106,17 @@ var RoomSubList = React.createClass({
|
||||||
|
|
||||||
applySearchFilter: function(list, filter) {
|
applySearchFilter: function(list, filter) {
|
||||||
if (filter === "") return list;
|
if (filter === "") return list;
|
||||||
return list.filter((room) => {
|
const lcFilter = filter.toLowerCase();
|
||||||
return room.name && room.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0
|
// case insensitive if room name includes filter,
|
||||||
});
|
// or if starts with `#` and one of room's aliases starts with filter
|
||||||
|
return list.filter((room) => (room.name && room.name.toLowerCase().includes(lcFilter)) ||
|
||||||
|
(filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))));
|
||||||
},
|
},
|
||||||
|
|
||||||
// The header is collapsable if it is hidden or not stuck
|
// The header is collapsable if it is hidden or not stuck
|
||||||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||||
isCollapsableOnClick: function() {
|
isCollapsableOnClick: function() {
|
||||||
var stuck = this.refs.header.dataset.stuck;
|
const stuck = this.refs.header.dataset.stuck;
|
||||||
if (this.state.hidden || stuck === undefined || stuck === "none") {
|
if (this.state.hidden || stuck === undefined || stuck === "none") {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -139,7 +142,7 @@ var RoomSubList = React.createClass({
|
||||||
onClick: function(ev) {
|
onClick: function(ev) {
|
||||||
if (this.isCollapsableOnClick()) {
|
if (this.isCollapsableOnClick()) {
|
||||||
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
|
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
|
||||||
var isHidden = !this.state.hidden;
|
const isHidden = !this.state.hidden;
|
||||||
this.setState({hidden: isHidden});
|
this.setState({hidden: isHidden});
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
|
@ -159,7 +162,7 @@ var RoomSubList = React.createClass({
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
clear_search: (ev && (ev.keyCode == KeyCode.ENTER || ev.keyCode == KeyCode.SPACE)),
|
clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -169,17 +172,17 @@ var RoomSubList = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_shouldShowMentionBadge: function(roomNotifState) {
|
_shouldShowMentionBadge: function(roomNotifState) {
|
||||||
return roomNotifState != RoomNotifs.MUTE;
|
return roomNotifState !== RoomNotifs.MUTE;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Total up all the notification counts from the rooms
|
* Total up all the notification counts from the rooms
|
||||||
*
|
*
|
||||||
* @param {Number} If supplied will only total notifications for rooms outside the truncation number
|
* @param {Number} truncateAt If supplied will only total notifications for rooms outside the truncation number
|
||||||
* @returns {Array} The array takes the form [total, highlight] where highlight is a bool
|
* @returns {Array} The array takes the form [total, highlight] where highlight is a bool
|
||||||
*/
|
*/
|
||||||
roomNotificationCount: function(truncateAt) {
|
roomNotificationCount: function(truncateAt) {
|
||||||
var self = this;
|
const self = this;
|
||||||
|
|
||||||
if (this.props.isInvite) {
|
if (this.props.isInvite) {
|
||||||
return [0, true];
|
return [0, true];
|
||||||
|
@ -187,9 +190,9 @@ var RoomSubList = React.createClass({
|
||||||
|
|
||||||
return this.props.list.reduce(function(result, room, index) {
|
return this.props.list.reduce(function(result, room, index) {
|
||||||
if (truncateAt === undefined || index >= truncateAt) {
|
if (truncateAt === undefined || index >= truncateAt) {
|
||||||
var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||||
var highlight = room.getUnreadNotificationCount('highlight') > 0;
|
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||||
var notificationCount = room.getUnreadNotificationCount();
|
const notificationCount = room.getUnreadNotificationCount();
|
||||||
|
|
||||||
const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
|
const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
|
||||||
const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState);
|
const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState);
|
||||||
|
@ -238,38 +241,83 @@ var RoomSubList = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onNotifBadgeClick: function(e) {
|
||||||
|
// prevent the roomsublist collapsing
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
// find first room which has notifications and switch to it
|
||||||
|
for (const room of this.state.sortedList) {
|
||||||
|
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||||
|
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||||
|
const notificationCount = room.getUnreadNotificationCount();
|
||||||
|
|
||||||
|
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(roomNotifState);
|
||||||
|
const mentionBadges = highlight && this._shouldShowMentionBadge(roomNotifState);
|
||||||
|
|
||||||
|
if (notifBadges || mentionBadges) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: room.roomId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onInviteBadgeClick: function(e) {
|
||||||
|
// prevent the roomsublist collapsing
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
// switch to first room in sortedList as that'll be the top of the list for the user
|
||||||
|
if (this.state.sortedList && this.state.sortedList.length > 0) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: this.state.sortedList[0].roomId,
|
||||||
|
});
|
||||||
|
} else if (this.props.extraTiles && this.props.extraTiles.length > 0) {
|
||||||
|
// Group Invites are different in that they are all extra tiles and not rooms
|
||||||
|
// XXX: this is a horrible special case because Group Invite sublist is a hack
|
||||||
|
if (this.props.extraTiles[0].props && this.props.extraTiles[0].props.group instanceof Group) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_group',
|
||||||
|
group_id: this.props.extraTiles[0].props.group.groupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_getHeaderJsx: function() {
|
_getHeaderJsx: function() {
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const subListNotifications = this.roomNotificationCount();
|
||||||
|
const subListNotifCount = subListNotifications[0];
|
||||||
|
const subListNotifHighlight = subListNotifications[1];
|
||||||
|
|
||||||
var subListNotifications = this.roomNotificationCount();
|
const totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
||||||
var subListNotifCount = subListNotifications[0];
|
const roomCount = totalTiles > 0 ? totalTiles : '';
|
||||||
var subListNotifHighlight = subListNotifications[1];
|
|
||||||
|
|
||||||
var totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
const chevronClasses = classNames({
|
||||||
var roomCount = totalTiles > 0 ? totalTiles : '';
|
|
||||||
|
|
||||||
var chevronClasses = classNames({
|
|
||||||
'mx_RoomSubList_chevron': true,
|
'mx_RoomSubList_chevron': true,
|
||||||
'mx_RoomSubList_chevronRight': this.state.hidden,
|
'mx_RoomSubList_chevronRight': this.state.hidden,
|
||||||
'mx_RoomSubList_chevronDown': !this.state.hidden,
|
'mx_RoomSubList_chevronDown': !this.state.hidden,
|
||||||
});
|
});
|
||||||
|
|
||||||
var badgeClasses = classNames({
|
const badgeClasses = classNames({
|
||||||
'mx_RoomSubList_badge': true,
|
'mx_RoomSubList_badge': true,
|
||||||
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
||||||
});
|
});
|
||||||
|
|
||||||
var badge;
|
let badge;
|
||||||
if (subListNotifCount > 0) {
|
if (subListNotifCount > 0) {
|
||||||
badge = <div className={badgeClasses}>{ FormattingUtils.formatCount(subListNotifCount) }</div>;
|
badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
|
||||||
|
{ FormattingUtils.formatCount(subListNotifCount) }
|
||||||
|
</div>;
|
||||||
} else if (this.props.isInvite) {
|
} else if (this.props.isInvite) {
|
||||||
// no notifications but highlight anyway because this is an invite badge
|
// no notifications but highlight anyway because this is an invite badge
|
||||||
badge = <div className={badgeClasses}>!</div>;
|
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>!</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When collapsed, allow a long hover on the header to show user
|
// When collapsed, allow a long hover on the header to show user
|
||||||
// the full tag name and room count
|
// the full tag name and room count
|
||||||
var title;
|
let title;
|
||||||
if (this.props.collapsed) {
|
if (this.props.collapsed) {
|
||||||
title = this.props.label;
|
title = this.props.label;
|
||||||
if (roomCount !== '') {
|
if (roomCount !== '') {
|
||||||
|
@ -277,28 +325,30 @@ var RoomSubList = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var incomingCall;
|
let incomingCall;
|
||||||
if (this.props.incomingCall) {
|
if (this.props.incomingCall) {
|
||||||
var self = this;
|
const self = this;
|
||||||
// Check if the incoming call is for this section
|
// Check if the incoming call is for this section
|
||||||
var incomingCallRoom = this.props.list.filter(function(room) {
|
const incomingCallRoom = this.props.list.filter(function(room) {
|
||||||
return self.props.incomingCall.roomId === room.roomId;
|
return self.props.incomingCall.roomId === room.roomId;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (incomingCallRoom.length === 1) {
|
if (incomingCallRoom.length === 1) {
|
||||||
var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||||
incomingCall = <IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={ this.props.incomingCall }/>;
|
incomingCall =
|
||||||
|
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
const tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||||
|
|
||||||
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomSubList_labelContainer" title={title} ref="header">
|
<div className="mx_RoomSubList_labelContainer" title={title} ref="header">
|
||||||
<AccessibleButton onClick={this.onClick} className="mx_RoomSubList_label" tabIndex={tabindex}>
|
<AccessibleButton onClick={this.onClick} className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||||
{this.props.collapsed ? '' : this.props.label}
|
{this.props.collapsed ? '' : this.props.label}
|
||||||
<div className="mx_RoomSubList_roomCount">{roomCount}</div>
|
<div className="mx_RoomSubList_roomCount">{roomCount}</div>
|
||||||
<div className={chevronClasses}></div>
|
<div className={chevronClasses} />
|
||||||
{badge}
|
{badge}
|
||||||
{incomingCall}
|
{incomingCall}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -307,24 +357,25 @@ var RoomSubList = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_createOverflowTile: function(overflowCount, totalCount) {
|
_createOverflowTile: function(overflowCount, totalCount) {
|
||||||
var content = <div className="mx_RoomSubList_chevronDown"></div>;
|
let content = <div className="mx_RoomSubList_chevronDown" />;
|
||||||
|
|
||||||
var overflowNotifications = this.roomNotificationCount(TRUNCATE_AT);
|
const overflowNotifications = this.roomNotificationCount(TRUNCATE_AT);
|
||||||
var overflowNotifCount = overflowNotifications[0];
|
const overflowNotifCount = overflowNotifications[0];
|
||||||
var overflowNotifHighlight = overflowNotifications[1];
|
const overflowNotifHighlight = overflowNotifications[1];
|
||||||
if (overflowNotifCount && !this.props.collapsed) {
|
if (overflowNotifCount && !this.props.collapsed) {
|
||||||
content = FormattingUtils.formatCount(overflowNotifCount);
|
content = FormattingUtils.formatCount(overflowNotifCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
var badgeClasses = classNames({
|
const badgeClasses = classNames({
|
||||||
'mx_RoomSubList_moreBadge': true,
|
'mx_RoomSubList_moreBadge': true,
|
||||||
'mx_RoomSubList_moreBadgeNotify': overflowNotifCount && !this.props.collapsed,
|
'mx_RoomSubList_moreBadgeNotify': overflowNotifCount && !this.props.collapsed,
|
||||||
'mx_RoomSubList_moreBadgeHighlight': overflowNotifHighlight && !this.props.collapsed,
|
'mx_RoomSubList_moreBadgeHighlight': overflowNotifHighlight && !this.props.collapsed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className="mx_RoomSubList_ellipsis" onClick={this._showFullMemberList}>
|
<AccessibleButton className="mx_RoomSubList_ellipsis" onClick={this._showFullMemberList}>
|
||||||
<div className="mx_RoomSubList_line"></div>
|
<div className="mx_RoomSubList_line" />
|
||||||
<div className="mx_RoomSubList_more">{_t("more")}</div>
|
<div className="mx_RoomSubList_more">{_t("more")}</div>
|
||||||
<div className={badgeClasses}>{content}</div>
|
<div className={badgeClasses}>{content}</div>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -333,7 +384,7 @@ var RoomSubList = React.createClass({
|
||||||
|
|
||||||
_showFullMemberList: function() {
|
_showFullMemberList: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
truncateAt: -1
|
truncateAt: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.onShowMoreRooms();
|
this.props.onShowMoreRooms();
|
||||||
|
@ -341,30 +392,44 @@ var RoomSubList = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var connectDropTarget = this.props.connectDropTarget;
|
const TruncatedList = sdk.getComponent('elements.TruncatedList');
|
||||||
var TruncatedList = sdk.getComponent('elements.TruncatedList');
|
|
||||||
|
|
||||||
var label = this.props.collapsed ? null : this.props.label;
|
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
|
if (this.props.showEmpty) {
|
||||||
|
// this is new behaviour with still controversial UX in that in hiding RoomSubLists the drop zones for DnD
|
||||||
|
// are also gone so when filtering users can't DnD rooms to some tags but is a lot cleaner otherwise.
|
||||||
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
||||||
content = this.props.emptyContent;
|
content = this.props.emptyContent;
|
||||||
} else {
|
} else {
|
||||||
content = this.makeRoomTiles();
|
content = this.makeRoomTiles();
|
||||||
content.push(...this.props.extraTiles);
|
content.push(...this.props.extraTiles);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (this.state.sortedList.length === 0 && this.props.extraTiles.length === 0) {
|
||||||
|
// if no search filter is applied and there is a placeholder defined then show it, otherwise show nothing
|
||||||
|
if (!this.props.searchFilter && this.props.emptyContent) {
|
||||||
|
content = this.props.emptyContent;
|
||||||
|
} else {
|
||||||
|
// don't show an empty sublist
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = this.makeRoomTiles();
|
||||||
|
content.push(...this.props.extraTiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.sortedList.length > 0 || this.props.extraTiles.length > 0 || this.props.editable) {
|
if (this.state.sortedList.length > 0 || this.props.extraTiles.length > 0 || this.props.editable) {
|
||||||
var subList;
|
let subList;
|
||||||
var classes = "mx_RoomSubList";
|
const classes = "mx_RoomSubList";
|
||||||
|
|
||||||
if (!this.state.hidden) {
|
if (!this.state.hidden) {
|
||||||
subList = <TruncatedList className={classes} truncateAt={this.state.truncateAt}
|
subList = <TruncatedList className={classes} truncateAt={this.state.truncateAt}
|
||||||
createOverflowElement={this._createOverflowTile}>
|
createOverflowElement={this._createOverflowTile}>
|
||||||
{content}
|
{content}
|
||||||
</TruncatedList>;
|
</TruncatedList>;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
subList = <TruncatedList className={classes}>
|
subList = <TruncatedList className={classes}>
|
||||||
</TruncatedList>;
|
</TruncatedList>;
|
||||||
}
|
}
|
||||||
|
@ -385,17 +450,20 @@ var RoomSubList = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Droppable> : subListContent;
|
</Droppable> : subListContent;
|
||||||
|
} else {
|
||||||
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
if (this.props.showSpinner) {
|
||||||
|
content = <Loader />;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomSubList">
|
<div className="mx_RoomSubList">
|
||||||
{this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined}
|
{this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined}
|
||||||
{ (this.props.showSpinner && !this.state.hidden) ? <Loader /> : undefined }
|
{ this.state.hidden ? undefined : content }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = RoomSubList;
|
module.exports = RoomSubList;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -44,7 +45,8 @@ import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
|
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||||
|
import WidgetUtils from '../../utils/WidgetUtils';
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function() {};
|
let debuglog = function() {};
|
||||||
|
@ -115,6 +117,7 @@ module.exports = React.createClass({
|
||||||
showApps: false,
|
showApps: false,
|
||||||
isAlone: false,
|
isAlone: false,
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
|
showingPinned: false,
|
||||||
|
|
||||||
// error object, as from the matrix client/server API
|
// error object, as from the matrix client/server API
|
||||||
// If we failed to load information about the room,
|
// If we failed to load information about the room,
|
||||||
|
@ -182,6 +185,8 @@ module.exports = React.createClass({
|
||||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||||
shouldPeek: RoomViewStore.shouldPeek(),
|
shouldPeek: RoomViewStore.shouldPeek(),
|
||||||
|
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()),
|
||||||
|
editingRoomSettings: RoomViewStore.isEditingSettings(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||||
|
@ -314,14 +319,7 @@ module.exports = React.createClass({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
return WidgetUtils.getRoomWidgets(room).length > 0;
|
||||||
// any valid widget = show apps
|
|
||||||
for (let i = 0; i < appsStateEvents.length; i++) {
|
|
||||||
if (appsStateEvents[i].getContent().type && appsStateEvents[i].getContent().url) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -615,9 +613,11 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_updatePreviewUrlVisibility: function(room) {
|
_updatePreviewUrlVisibility: function({roomId}) {
|
||||||
|
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
|
||||||
|
const key = MatrixClientPeg.get().isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
|
||||||
this.setState({
|
this.setState({
|
||||||
showUrlPreview: SettingsStore.getValue("urlPreviewsEnabled", room.roomId),
|
showUrlPreview: SettingsStore.getValue(key, roomId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -642,19 +642,23 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onAccountData: function(event) {
|
onAccountData: function(event) {
|
||||||
if (event.getType() === "org.matrix.preview_urls" && this.state.room) {
|
const type = event.getType();
|
||||||
|
if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) {
|
||||||
|
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||||
this._updatePreviewUrlVisibility(this.state.room);
|
this._updatePreviewUrlVisibility(this.state.room);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomAccountData: function(event, room) {
|
onRoomAccountData: function(event, room) {
|
||||||
if (room.roomId == this.state.roomId) {
|
if (room.roomId == this.state.roomId) {
|
||||||
if (event.getType() === "org.matrix.room.color_scheme") {
|
const type = event.getType();
|
||||||
|
if (type === "org.matrix.room.color_scheme") {
|
||||||
const color_scheme = event.getContent();
|
const color_scheme = event.getContent();
|
||||||
// XXX: we should validate the event
|
// XXX: we should validate the event
|
||||||
console.log("Tinter.tint from onRoomAccountData");
|
console.log("Tinter.tint from onRoomAccountData");
|
||||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||||
} else if (event.getType() === "org.matrix.room.preview_urls") {
|
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
|
||||||
|
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||||
this._updatePreviewUrlVisibility(room);
|
this._updatePreviewUrlVisibility(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -672,6 +676,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
this._updateRoomMembers();
|
this._updateRoomMembers();
|
||||||
|
this._checkIfAlone(this.state.room);
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomMemberMembership: function(ev, member, oldMembership) {
|
onRoomMemberMembership: function(ev, member, oldMembership) {
|
||||||
|
@ -909,6 +914,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadFile: async function(file) {
|
uploadFile: async function(file) {
|
||||||
|
dis.dispatch({action: 'focus_composer'});
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
dis.dispatch({action: 'view_set_mxid'});
|
dis.dispatch({action: 'view_set_mxid'});
|
||||||
return;
|
return;
|
||||||
|
@ -1135,11 +1142,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onPinnedClick: function() {
|
onPinnedClick: function() {
|
||||||
this.setState({showingPinned: !this.state.showingPinned, searching: false});
|
const nowShowingPinned = !this.state.showingPinned;
|
||||||
|
const roomId = this.state.room.roomId;
|
||||||
|
this.setState({showingPinned: nowShowingPinned, searching: false});
|
||||||
|
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
|
||||||
},
|
},
|
||||||
|
|
||||||
onSettingsClick: function() {
|
onSettingsClick: function() {
|
||||||
this.showSettings(true);
|
dis.dispatch({ action: 'open_room_settings' });
|
||||||
},
|
},
|
||||||
|
|
||||||
onSettingsSaveClick: function() {
|
onSettingsSaveClick: function() {
|
||||||
|
@ -1172,24 +1182,20 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
// still editing room settings
|
// still editing room settings
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
dis.dispatch({ action: 'close_settings' });
|
||||||
editingRoomSettings: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
uploadingRoomSettings: false,
|
uploadingRoomSettings: false,
|
||||||
editingRoomSettings: false,
|
|
||||||
});
|
});
|
||||||
|
dis.dispatch({ action: 'close_settings' });
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
onCancelClick: function() {
|
onCancelClick: function() {
|
||||||
console.log("updateTint from onCancelClick");
|
console.log("updateTint from onCancelClick");
|
||||||
this.updateTint();
|
this.updateTint();
|
||||||
this.setState({
|
dis.dispatch({ action: 'close_settings' });
|
||||||
editingRoomSettings: false,
|
|
||||||
});
|
|
||||||
if (this.state.forwardingEvent) {
|
if (this.state.forwardingEvent) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'forward_event',
|
action: 'forward_event',
|
||||||
|
@ -1406,13 +1412,6 @@ module.exports = React.createClass({
|
||||||
});*/
|
});*/
|
||||||
},
|
},
|
||||||
|
|
||||||
showSettings: function(show) {
|
|
||||||
// XXX: this is a bit naughty; we should be doing this via props
|
|
||||||
if (show) {
|
|
||||||
this.setState({editingRoomSettings: true});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* called by the parent component when PageUp/Down/etc is pressed.
|
* called by the parent component when PageUp/Down/etc is pressed.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd.
|
Copyright 2017, 2018 New Vector Ltd.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -26,6 +26,7 @@ import dis from '../../dispatcher';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
import { Droppable } from 'react-beautiful-dnd';
|
import { Droppable } from 'react-beautiful-dnd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const TagPanel = React.createClass({
|
const TagPanel = React.createClass({
|
||||||
displayName: 'TagPanel',
|
displayName: 'TagPanel',
|
||||||
|
@ -84,7 +85,10 @@ const TagPanel = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseDown(e) {
|
onMouseDown(e) {
|
||||||
|
// only dispatch if its not a no-op
|
||||||
|
if (this.state.selectedTags.length > 0) {
|
||||||
dis.dispatch({action: 'deselect_tags'});
|
dis.dispatch({action: 'deselect_tags'});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onCreateGroupClick(ev) {
|
onCreateGroupClick(ev) {
|
||||||
|
@ -113,17 +117,26 @@ const TagPanel = React.createClass({
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const clearButton = this.state.selectedTags.length > 0 ?
|
const itemsSelected = this.state.selectedTags.length > 0;
|
||||||
|
|
||||||
|
let clearButton;
|
||||||
|
if (itemsSelected) {
|
||||||
|
clearButton = <AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||||
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
||||||
alt={_t("Clear filter")}
|
alt={_t("Clear filter")}
|
||||||
title={_t("Clear filter")}
|
title={_t("Clear filter")}
|
||||||
/> :
|
/>
|
||||||
<div />;
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="mx_TagPanel">
|
const classes = classNames('mx_TagPanel', {
|
||||||
<AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
mx_TagPanel_items_selected: itemsSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={classes}>
|
||||||
|
<div className="mx_TagPanel_clearButton_container">
|
||||||
{ clearButton }
|
{ clearButton }
|
||||||
</AccessibleButton>
|
</div>
|
||||||
<div className="mx_TagPanel_divider" />
|
<div className="mx_TagPanel_divider" />
|
||||||
<GeminiScrollbarWrapper
|
<GeminiScrollbarWrapper
|
||||||
className="mx_TagPanel_scroller"
|
className="mx_TagPanel_scroller"
|
||||||
|
|
|
@ -81,6 +81,7 @@ const SIMPLE_SETTINGS = [
|
||||||
{ id: "VideoView.flipVideoHorizontally" },
|
{ id: "VideoView.flipVideoHorizontally" },
|
||||||
{ id: "TagPanel.disableTagPanel" },
|
{ id: "TagPanel.disableTagPanel" },
|
||||||
{ id: "enableWidgetScreenshots" },
|
{ id: "enableWidgetScreenshots" },
|
||||||
|
{ id: "RoomSubList.showEmpty" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// These settings must be defined in SettingsStore
|
// These settings must be defined in SettingsStore
|
||||||
|
@ -284,7 +285,13 @@ module.exports = React.createClass({
|
||||||
this.setState({ electron_settings: settings });
|
this.setState({ electron_settings: settings });
|
||||||
},
|
},
|
||||||
|
|
||||||
_refreshMediaDevices: function() {
|
_refreshMediaDevices: function(stream) {
|
||||||
|
if (stream) {
|
||||||
|
// kill stream so that we don't leave it lingering around with webcam enabled etc
|
||||||
|
// as here we called gUM to ask user for permission to their device names only
|
||||||
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
return CallMediaHandler.getDevices();
|
return CallMediaHandler.getDevices();
|
||||||
}).then((mediaDevices) => {
|
}).then((mediaDevices) => {
|
||||||
|
@ -292,6 +299,7 @@ module.exports = React.createClass({
|
||||||
if (this._unmounted) return;
|
if (this._unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
|
activeAudioOutput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audiooutput'),
|
||||||
activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
|
activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
|
||||||
activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
|
activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
|
||||||
});
|
});
|
||||||
|
@ -422,7 +430,6 @@ module.exports = React.createClass({
|
||||||
"push notifications on other devices until you log back in to them",
|
"push notifications on other devices until you log back in to them",
|
||||||
) + ".",
|
) + ".",
|
||||||
});
|
});
|
||||||
dis.dispatch({action: 'password_changed'});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
||||||
|
@ -970,6 +977,11 @@ module.exports = React.createClass({
|
||||||
return devices.map((device) => <span key={device.deviceId}>{ device.label }</span>);
|
return devices.map((device) => <span key={device.deviceId}>{ device.label }</span>);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_setAudioOutput: function(deviceId) {
|
||||||
|
this.setState({activeAudioOutput: deviceId});
|
||||||
|
CallMediaHandler.setAudioOutput(deviceId);
|
||||||
|
},
|
||||||
|
|
||||||
_setAudioInput: function(deviceId) {
|
_setAudioInput: function(deviceId) {
|
||||||
this.setState({activeAudioInput: deviceId});
|
this.setState({activeAudioInput: deviceId});
|
||||||
CallMediaHandler.setAudioInput(deviceId);
|
CallMediaHandler.setAudioInput(deviceId);
|
||||||
|
@ -1010,6 +1022,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||||
|
|
||||||
|
let speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>;
|
||||||
let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
||||||
let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
||||||
|
|
||||||
|
@ -1018,6 +1031,26 @@ module.exports = React.createClass({
|
||||||
label: _t('Default Device'),
|
label: _t('Default Device'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
|
||||||
|
if (audioOutputs.length > 0) {
|
||||||
|
let defaultOutput = '';
|
||||||
|
if (!audioOutputs.some((input) => input.deviceId === 'default')) {
|
||||||
|
audioOutputs.unshift(defaultOption);
|
||||||
|
} else {
|
||||||
|
defaultOutput = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
speakerDropdown = <div>
|
||||||
|
<h4>{ _t('Audio Output') }</h4>
|
||||||
|
<Dropdown
|
||||||
|
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||||
|
value={this.state.activeAudioOutput || defaultOutput}
|
||||||
|
onOptionChange={this._setAudioOutput}>
|
||||||
|
{ this._mapWebRtcDevicesToSpans(audioOutputs) }
|
||||||
|
</Dropdown>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
|
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
|
||||||
if (audioInputs.length > 0) {
|
if (audioInputs.length > 0) {
|
||||||
let defaultInput = '';
|
let defaultInput = '';
|
||||||
|
@ -1059,6 +1092,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
|
{ speakerDropdown }
|
||||||
{ microphoneDropdown }
|
{ microphoneDropdown }
|
||||||
{ webcamDropdown }
|
{ webcamDropdown }
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -1074,6 +1108,14 @@ module.exports = React.createClass({
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onSelfShareClick: function() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||||
|
Modal.createTrackedDialog('share self dialog', '', ShareDialog, {
|
||||||
|
target: cli.getUser(this._me),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_showSpoiler: function(event) {
|
_showSpoiler: function(event) {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
target.innerHTML = target.getAttribute('data-spoiler');
|
target.innerHTML = target.getAttribute('data-spoiler');
|
||||||
|
@ -1295,10 +1337,13 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
{ _t("Logged in as:") } { this._me }
|
{ _t("Logged in as:") + ' ' }
|
||||||
|
<a onClick={this.onSelfShareClick} className="mx_UserSettings_link">
|
||||||
|
{ this._me }
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
{ _t('Access Token:') }
|
{ _t('Access Token:') + ' ' }
|
||||||
<span className="mx_UserSettings_advanced_spoiler"
|
<span className="mx_UserSettings_advanced_spoiler"
|
||||||
onClick={this._showSpoiler}
|
onClick={this._showSpoiler}
|
||||||
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -45,6 +43,8 @@ module.exports = React.createClass({
|
||||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||||
progress: null,
|
progress: null,
|
||||||
|
password: null,
|
||||||
|
password2: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ module.exports = React.createClass({
|
||||||
</div>,
|
</div>,
|
||||||
button: _t('Continue'),
|
button: _t('Continue'),
|
||||||
extraButtons: [
|
extraButtons: [
|
||||||
<button className="mx_Dialog_primary"
|
<button key="export_keys" className="mx_Dialog_primary"
|
||||||
onClick={this._onExportE2eKeysClicked}>
|
onClick={this._onExportE2eKeysClicked}>
|
||||||
{ _t('Export E2E room keys') }
|
{ _t('Export E2E room keys') }
|
||||||
</button>,
|
</button>,
|
||||||
|
@ -169,7 +169,8 @@ module.exports = React.createClass({
|
||||||
} else if (this.state.progress === "sent_email") {
|
} else if (this.state.progress === "sent_email") {
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
<div className="mx_Login_prompt">
|
<div className="mx_Login_prompt">
|
||||||
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
|
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, " +
|
||||||
|
"click below.", { emailAddress: this.state.email }) }
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||||
value={_t('I have verified my email address')} />
|
value={_t('I have verified my email address')} />
|
||||||
|
@ -179,14 +180,15 @@ module.exports = React.createClass({
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
<div className="mx_Login_prompt">
|
<div className="mx_Login_prompt">
|
||||||
<p>{ _t('Your password has been reset') }.</p>
|
<p>{ _t('Your password has been reset') }.</p>
|
||||||
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
|
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. ' +
|
||||||
|
'To re-enable notifications, sign in again on each device') }.</p>
|
||||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||||
value={_t('Return to login screen')} />
|
value={_t('Return to login screen')} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let serverConfigSection;
|
let serverConfigSection;
|
||||||
if (!SdkConfig.get().disable_custom_urls) {
|
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||||
serverConfigSection = (
|
serverConfigSection = (
|
||||||
<ServerConfig ref="serverConfig"
|
<ServerConfig ref="serverConfig"
|
||||||
withToggleButton={true}
|
withToggleButton={true}
|
||||||
|
@ -199,6 +201,8 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||||
|
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_Login_prompt">
|
<div className="mx_Login_prompt">
|
||||||
|
@ -233,6 +237,7 @@ module.exports = React.createClass({
|
||||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||||
{ _t('Create an account') }
|
{ _t('Create an account') }
|
||||||
</a>
|
</a>
|
||||||
|
<LanguageSelector />
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
38
src/components/structures/login/LanguageSelector.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
import {getCurrentLanguage} from "../../../languageHandler";
|
||||||
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
|
import PlatformPeg from "../../../PlatformPeg";
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function onChange(newLang) {
|
||||||
|
if (getCurrentLanguage() !== newLang) {
|
||||||
|
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||||
|
PlatformPeg.get().reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LanguageSelector() {
|
||||||
|
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
||||||
|
|
||||||
|
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||||
|
return <div className="mx_Login_language_div">
|
||||||
|
<LanguageDropdown onOptionChange={onChange} className="mx_Login_language" value={getCurrentLanguage()} />
|
||||||
|
</div>;
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -20,15 +21,13 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as languageHandler from '../../../languageHandler';
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Login from '../../../Login';
|
import Login from '../../../Login';
|
||||||
import PlatformPeg from '../../../PlatformPeg';
|
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
// For validating phone numbers without country codes
|
// For validating phone numbers without country codes
|
||||||
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wire component which glues together login UI components and Login logic
|
* A wire component which glues together login UI components and Login logic
|
||||||
|
@ -94,6 +93,13 @@ module.exports = React.createClass({
|
||||||
this._unmounted = true;
|
this._unmounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPasswordLoginError: function(errorText) {
|
||||||
|
this.setState({
|
||||||
|
errorText,
|
||||||
|
loginIncorrect: Boolean(errorText),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -113,10 +119,10 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// Some error strings only apply for logging in
|
// Some error strings only apply for logging in
|
||||||
const usingEmail = username.indexOf("@") > 0;
|
const usingEmail = username.indexOf("@") > 0;
|
||||||
if (error.httpStatus == 400 && usingEmail) {
|
if (error.httpStatus === 400 && usingEmail) {
|
||||||
errorText = _t('This Home Server does not support login using email address.');
|
errorText = _t('This Home Server does not support login using email address.');
|
||||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
||||||
if (SdkConfig.get().disable_custom_urls) {
|
if (SdkConfig.get()['disable_custom_urls']) {
|
||||||
errorText = (
|
errorText = (
|
||||||
<div>
|
<div>
|
||||||
<div>{ _t('Incorrect username and/or password.') }</div>
|
<div>{ _t('Incorrect username and/or password.') }</div>
|
||||||
|
@ -143,7 +149,7 @@ module.exports = React.createClass({
|
||||||
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
|
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
|
||||||
// mentions this (although the bug is for UI auth which is not this)
|
// mentions this (although the bug is for UI auth which is not this)
|
||||||
// We treat both as an incorrect password
|
// We treat both as an incorrect password
|
||||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
|
loginIncorrect: error.httpStatus === 401 || error.httpStatus === 403,
|
||||||
});
|
});
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
if (this._unmounted) {
|
if (this._unmounted) {
|
||||||
|
@ -231,7 +237,7 @@ module.exports = React.createClass({
|
||||||
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||||
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
||||||
|
|
||||||
const fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||||
|
|
||||||
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||||
|
@ -310,19 +316,27 @@ module.exports = React.createClass({
|
||||||
!this.state.enteredHomeserverUrl.startsWith("http"))
|
!this.state.enteredHomeserverUrl.startsWith("http"))
|
||||||
) {
|
) {
|
||||||
errorText = <span>
|
errorText = <span>
|
||||||
|
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||||
|
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
|
||||||
{
|
{
|
||||||
_t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
'a': (sub) => {
|
||||||
"Either use HTTPS or <a>enable unsafe scripts</a>.",
|
return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">
|
||||||
{},
|
{ sub }
|
||||||
{ 'a': (sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; } },
|
</a>;
|
||||||
|
},
|
||||||
|
},
|
||||||
) }
|
) }
|
||||||
</span>;
|
</span>;
|
||||||
} else {
|
} else {
|
||||||
errorText = <span>
|
errorText = <span>
|
||||||
|
{ _t("Can't connect to homeserver - please check your connectivity, ensure your " +
|
||||||
|
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
|
||||||
|
"is not blocking requests.", {},
|
||||||
{
|
{
|
||||||
_t("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
'a': (sub) => {
|
||||||
{},
|
return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>;
|
||||||
{ 'a': (sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; } },
|
},
|
||||||
|
},
|
||||||
) }
|
) }
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
@ -350,6 +364,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<PasswordLogin
|
<PasswordLogin
|
||||||
onSubmit={this.onPasswordLogin}
|
onSubmit={this.onPasswordLogin}
|
||||||
|
onError={this.onPasswordLoginError}
|
||||||
initialUsername={this.state.username}
|
initialUsername={this.state.username}
|
||||||
initialPhoneCountry={this.state.phoneCountry}
|
initialPhoneCountry={this.state.phoneCountry}
|
||||||
initialPhoneNumber={this.state.phoneNumber}
|
initialPhoneNumber={this.state.phoneNumber}
|
||||||
|
@ -370,23 +385,6 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_onLanguageChange: function(newLang) {
|
|
||||||
if (languageHandler.getCurrentLanguage() !== newLang) {
|
|
||||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
|
||||||
PlatformPeg.get().reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderLanguageSetting: function() {
|
|
||||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
|
||||||
return <div className="mx_Login_language_div">
|
|
||||||
<LanguageDropdown onOptionChange={this._onLanguageChange}
|
|
||||||
className="mx_Login_language"
|
|
||||||
value={languageHandler.getCurrentLanguage()}
|
|
||||||
/>
|
|
||||||
</div>;
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
const LoginPage = sdk.getComponent("login.LoginPage");
|
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||||
|
@ -399,25 +397,14 @@ module.exports = React.createClass({
|
||||||
if (this.props.enableGuest) {
|
if (this.props.enableGuest) {
|
||||||
loginAsGuestJsx =
|
loginAsGuestJsx =
|
||||||
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
||||||
{ _t('Login as guest') }
|
{ _t('Try the app first') }
|
||||||
</a>;
|
</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let returnToAppJsx;
|
|
||||||
/*
|
|
||||||
// with the advent of ILAG I don't think we need this any more
|
|
||||||
if (this.props.onCancelClick) {
|
|
||||||
returnToAppJsx =
|
|
||||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
|
||||||
{ _t('Return to app') }
|
|
||||||
</a>;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
let serverConfig;
|
let serverConfig;
|
||||||
let header;
|
let header;
|
||||||
|
|
||||||
if (!SdkConfig.get().disable_custom_urls) {
|
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||||
serverConfig = <ServerConfig ref="serverConfig"
|
serverConfig = <ServerConfig ref="serverConfig"
|
||||||
withToggleButton={true}
|
withToggleButton={true}
|
||||||
customHsUrl={this.props.customHsUrl}
|
customHsUrl={this.props.customHsUrl}
|
||||||
|
@ -447,6 +434,8 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginPage>
|
<LoginPage>
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
|
@ -460,8 +449,7 @@ module.exports = React.createClass({
|
||||||
{ _t('Create an account') }
|
{ _t('Create an account') }
|
||||||
</a>
|
</a>
|
||||||
{ loginAsGuestJsx }
|
{ loginAsGuestJsx }
|
||||||
{ returnToAppJsx }
|
<LanguageSelector />
|
||||||
{ !SdkConfig.get().disable_login_language_selector ? this._renderLanguageSetting() : '' }
|
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -22,7 +23,6 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import ServerConfig from '../../views/login/ServerConfig';
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import RegistrationForm from '../../views/login/RegistrationForm';
|
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||||
import RtsClient from '../../../RtsClient';
|
import RtsClient from '../../../RtsClient';
|
||||||
|
@ -62,6 +62,12 @@ module.exports = React.createClass({
|
||||||
onLoginClick: PropTypes.func.isRequired,
|
onLoginClick: PropTypes.func.isRequired,
|
||||||
onCancelClick: PropTypes.func,
|
onCancelClick: PropTypes.func,
|
||||||
onServerConfigChange: PropTypes.func.isRequired,
|
onServerConfigChange: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
rtsClient: PropTypes.shape({
|
||||||
|
getTeamsConfig: PropTypes.func.isRequired,
|
||||||
|
trackReferral: PropTypes.func.isRequired,
|
||||||
|
getTeam: PropTypes.func.isRequired,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -133,7 +139,7 @@ module.exports = React.createClass({
|
||||||
newState.isUrl = config.isUrl;
|
newState.isUrl = config.isUrl;
|
||||||
}
|
}
|
||||||
this.props.onServerConfigChange(config);
|
this.props.onServerConfigChange(config);
|
||||||
this.setState(newState, function() {
|
this.setState(newState, () => {
|
||||||
this._replaceClient();
|
this._replaceClient();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -159,11 +165,11 @@ module.exports = React.createClass({
|
||||||
let msg = response.message || response.toString();
|
let msg = response.message || response.toString();
|
||||||
// can we give a better error message?
|
// can we give a better error message?
|
||||||
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||||
let msisdn_available = false;
|
let msisdnAvailable = false;
|
||||||
for (const flow of response.available_flows) {
|
for (const flow of response.available_flows) {
|
||||||
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
msisdnAvailable |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||||
}
|
}
|
||||||
if (!msisdn_available) {
|
if (!msisdnAvailable) {
|
||||||
msg = _t('This server does not support authentication with a phone number.');
|
msg = _t('This server does not support authentication with a phone number.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -242,7 +248,7 @@ module.exports = React.createClass({
|
||||||
return matrixClient.getPushers().then((resp)=>{
|
return matrixClient.getPushers().then((resp)=>{
|
||||||
const pushers = resp.pushers;
|
const pushers = resp.pushers;
|
||||||
for (let i = 0; i < pushers.length; ++i) {
|
for (let i = 0; i < pushers.length; ++i) {
|
||||||
if (pushers[i].kind == 'email') {
|
if (pushers[i].kind === 'email') {
|
||||||
const emailPusher = pushers[i];
|
const emailPusher = pushers[i];
|
||||||
emailPusher.data = { brand: this.props.brand };
|
emailPusher.data = { brand: this.props.brand };
|
||||||
matrixClient.setPusher(emailPusher).done(() => {
|
matrixClient.setPusher(emailPusher).done(() => {
|
||||||
|
@ -267,7 +273,7 @@ module.exports = React.createClass({
|
||||||
errMsg = _t('Passwords don\'t match.');
|
errMsg = _t('Passwords don\'t match.');
|
||||||
break;
|
break;
|
||||||
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||||
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH: MIN_PASSWORD_LENGTH});
|
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH});
|
||||||
break;
|
break;
|
||||||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||||
errMsg = _t('This doesn\'t look like a valid email address.');
|
errMsg = _t('This doesn\'t look like a valid email address.');
|
||||||
|
@ -353,7 +359,7 @@ module.exports = React.createClass({
|
||||||
registerBody = <Spinner />;
|
registerBody = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
let serverConfigSection;
|
let serverConfigSection;
|
||||||
if (!SdkConfig.get().disable_custom_urls) {
|
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||||
serverConfigSection = (
|
serverConfigSection = (
|
||||||
<ServerConfig ref="serverConfig"
|
<ServerConfig ref="serverConfig"
|
||||||
withToggleButton={true}
|
withToggleButton={true}
|
||||||
|
@ -385,18 +391,6 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let returnToAppJsx;
|
|
||||||
/*
|
|
||||||
// with the advent of ILAG I don't think we need this any more
|
|
||||||
if (this.props.onCancelClick) {
|
|
||||||
returnToAppJsx = (
|
|
||||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
|
||||||
{ _t('Return to app') }
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
let header;
|
let header;
|
||||||
let errorText;
|
let errorText;
|
||||||
// FIXME: remove hardcoded Status team tweaks at some point
|
// FIXME: remove hardcoded Status team tweaks at some point
|
||||||
|
@ -418,6 +412,8 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginPage>
|
<LoginPage>
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
|
@ -431,7 +427,7 @@ module.exports = React.createClass({
|
||||||
{ registerBody }
|
{ registerBody }
|
||||||
{ signIn }
|
{ signIn }
|
||||||
{ errorText }
|
{ errorText }
|
||||||
{ returnToAppJsx }
|
<LanguageSelector />
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
</LoginPage>
|
</LoginPage>
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
import {Group} from 'matrix-js-sdk';
|
||||||
|
import GroupStore from "../../../stores/GroupStore";
|
||||||
|
|
||||||
|
export default class GroupInviteTileContextMenu extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
group: PropTypes.instanceOf(Group).isRequired,
|
||||||
|
/* callback called when the menu is dismissed */
|
||||||
|
onFinished: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this._onClickReject = this._onClickReject.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this._unmounted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unmounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClickReject() {
|
||||||
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
|
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
|
||||||
|
title: _t('Reject invitation'),
|
||||||
|
description: _t('Are you sure you want to reject the invitation?'),
|
||||||
|
onFinished: async (shouldLeave) => {
|
||||||
|
if (!shouldLeave) return;
|
||||||
|
|
||||||
|
// FIXME: controller shouldn't be loading a view :(
|
||||||
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await GroupStore.leaveGroup(this.props.group.groupId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error rejecting community invite: ", e);
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
|
||||||
|
title: _t("Error"),
|
||||||
|
description: _t("Unable to reject invite"),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the context menu
|
||||||
|
if (this.props.onFinished) {
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div>
|
||||||
|
<div className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
|
||||||
|
<img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
||||||
|
{ _t('Reject') }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,10 +15,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import {EventStatus} from 'matrix-js-sdk';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
@ -184,6 +183,15 @@ module.exports = React.createClass({
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPermalinkClick: function(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||||
|
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
||||||
|
target: this.props.mxEvent,
|
||||||
|
});
|
||||||
|
this.closeMenu();
|
||||||
|
},
|
||||||
|
|
||||||
onReplyClick: function() {
|
onReplyClick: function() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'reply_to_event',
|
action: 'reply_to_event',
|
||||||
|
@ -211,7 +219,10 @@ module.exports = React.createClass({
|
||||||
let replyButton;
|
let replyButton;
|
||||||
let collapseReplyThread;
|
let collapseReplyThread;
|
||||||
|
|
||||||
if (eventStatus === 'not_sent') {
|
// status is SENT before remote-echo, null after
|
||||||
|
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||||
|
|
||||||
|
if (eventStatus === EventStatus.NOT_SENT) {
|
||||||
resendButton = (
|
resendButton = (
|
||||||
<div className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
<div className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||||
{ _t('Resend') }
|
{ _t('Resend') }
|
||||||
|
@ -219,7 +230,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!eventStatus && this.state.canRedact) {
|
if (isSent && this.state.canRedact) {
|
||||||
redactButton = (
|
redactButton = (
|
||||||
<div className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
<div className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||||
{ _t('Remove') }
|
{ _t('Remove') }
|
||||||
|
@ -227,7 +238,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventStatus === "queued" || eventStatus === "not_sent") {
|
if (eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT) {
|
||||||
cancelButton = (
|
cancelButton = (
|
||||||
<div className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
<div className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||||
{ _t('Cancel Sending') }
|
{ _t('Cancel Sending') }
|
||||||
|
@ -235,7 +246,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!eventStatus && this.props.mxEvent.getType() === 'm.room.message') {
|
if (isSent && this.props.mxEvent.getType() === 'm.room.message') {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
|
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
|
||||||
forwardButton = (
|
forwardButton = (
|
||||||
|
@ -244,13 +255,11 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (SettingsStore.isFeatureEnabled("feature_rich_quoting")) {
|
|
||||||
replyButton = (
|
replyButton = (
|
||||||
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
|
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
|
||||||
{ _t('Reply') }
|
{ _t('Reply') }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.canPin) {
|
if (this.state.canPin) {
|
||||||
pinButton = (
|
pinButton = (
|
||||||
|
@ -290,7 +299,7 @@ module.exports = React.createClass({
|
||||||
const permalinkButton = (
|
const permalinkButton = (
|
||||||
<div className="mx_MessageContextMenu_field">
|
<div className="mx_MessageContextMenu_field">
|
||||||
<a href={makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId())}
|
<a href={makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId())}
|
||||||
target="_blank" rel="noopener" onClick={this.closeMenu}>{ _t('Permalink') }</a>
|
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>{ _t('Share Message') }</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
@ -27,6 +27,13 @@ import GroupStore from '../../../stores/GroupStore';
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
const TRUNCATE_QUERY_LIST = 40;
|
||||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||||
|
|
||||||
|
const addressTypeName = {
|
||||||
|
'mx-user-id': _td("Matrix ID"),
|
||||||
|
'mx-room-id': _td("Matrix Room ID"),
|
||||||
|
'email': _td("email address"),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: "AddressPickerDialog",
|
displayName: "AddressPickerDialog",
|
||||||
|
|
||||||
|
@ -66,7 +73,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// List of UserAddressType objects representing
|
// List of UserAddressType objects representing
|
||||||
// the list of addresses we're going to invite
|
// the list of addresses we're going to invite
|
||||||
userList: [],
|
selectedList: [],
|
||||||
|
|
||||||
// Whether a search is ongoing
|
// Whether a search is ongoing
|
||||||
busy: false,
|
busy: false,
|
||||||
|
@ -76,10 +83,9 @@ module.exports = React.createClass({
|
||||||
serverSupportsUserDirectory: true,
|
serverSupportsUserDirectory: true,
|
||||||
// The query being searched for
|
// The query being searched for
|
||||||
query: "",
|
query: "",
|
||||||
// List of UserAddressType objects representing
|
// List of UserAddressType objects representing the set of
|
||||||
// the set of auto-completion results for the current search
|
// auto-completion results for the current search query.
|
||||||
// query.
|
suggestedList: [],
|
||||||
queryList: [],
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -91,14 +97,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onButtonClick: function() {
|
onButtonClick: function() {
|
||||||
let userList = this.state.userList.slice();
|
let selectedList = this.state.selectedList.slice();
|
||||||
// Check the text input field to see if user has an unconverted address
|
// Check the text input field to see if user has an unconverted address
|
||||||
// If there is and it's valid add it to the local userList
|
// If there is and it's valid add it to the local selectedList
|
||||||
if (this.refs.textinput.value !== '') {
|
if (this.refs.textinput.value !== '') {
|
||||||
userList = this._addInputToList();
|
selectedList = this._addInputToList();
|
||||||
if (userList === null) return;
|
if (selectedList === null) return;
|
||||||
}
|
}
|
||||||
this.props.onFinished(true, userList);
|
this.props.onFinished(true, selectedList);
|
||||||
},
|
},
|
||||||
|
|
||||||
onCancel: function() {
|
onCancel: function() {
|
||||||
|
@ -118,18 +124,18 @@ module.exports = React.createClass({
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
||||||
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
} else if (this.state.suggestedList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
if (this.addressSelector) this.addressSelector.chooseSelection();
|
||||||
} else if (this.refs.textinput.value.length === 0 && this.state.userList.length && e.keyCode === 8) { // backspace
|
} else if (this.refs.textinput.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.onDismissed(this.state.userList.length - 1)();
|
this.onDismissed(this.state.selectedList.length - 1)();
|
||||||
} else if (e.keyCode === 13) { // enter
|
} else if (e.keyCode === 13) { // enter
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.refs.textinput.value == '') {
|
if (this.refs.textinput.value === '') {
|
||||||
// if there's nothing in the input box, submit the form
|
// if there's nothing in the input box, submit the form
|
||||||
this.onButtonClick();
|
this.onButtonClick();
|
||||||
} else {
|
} else {
|
||||||
|
@ -148,7 +154,7 @@ module.exports = React.createClass({
|
||||||
clearTimeout(this.queryChangedDebouncer);
|
clearTimeout(this.queryChangedDebouncer);
|
||||||
}
|
}
|
||||||
// Only do search if there is something to search
|
// Only do search if there is something to search
|
||||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
if (query.length > 0 && query !== '@' && query.length >= 2) {
|
||||||
this.queryChangedDebouncer = setTimeout(() => {
|
this.queryChangedDebouncer = setTimeout(() => {
|
||||||
if (this.props.pickerType === 'user') {
|
if (this.props.pickerType === 'user') {
|
||||||
if (this.props.groupId) {
|
if (this.props.groupId) {
|
||||||
|
@ -170,7 +176,7 @@ module.exports = React.createClass({
|
||||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
queryList: [],
|
suggestedList: [],
|
||||||
query: "",
|
query: "",
|
||||||
searchError: null,
|
searchError: null,
|
||||||
});
|
});
|
||||||
|
@ -179,11 +185,11 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onDismissed: function(index) {
|
onDismissed: function(index) {
|
||||||
return () => {
|
return () => {
|
||||||
const userList = this.state.userList.slice();
|
const selectedList = this.state.selectedList.slice();
|
||||||
userList.splice(index, 1);
|
selectedList.splice(index, 1);
|
||||||
this.setState({
|
this.setState({
|
||||||
userList: userList,
|
selectedList,
|
||||||
queryList: [],
|
suggestedList: [],
|
||||||
query: "",
|
query: "",
|
||||||
});
|
});
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
|
@ -197,11 +203,11 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onSelected: function(index) {
|
onSelected: function(index) {
|
||||||
const userList = this.state.userList.slice();
|
const selectedList = this.state.selectedList.slice();
|
||||||
userList.push(this.state.queryList[index]);
|
selectedList.push(this.state.suggestedList[index]);
|
||||||
this.setState({
|
this.setState({
|
||||||
userList: userList,
|
selectedList,
|
||||||
queryList: [],
|
suggestedList: [],
|
||||||
query: "",
|
query: "",
|
||||||
});
|
});
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
|
@ -379,10 +385,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_processResults: function(results, query) {
|
_processResults: function(results, query) {
|
||||||
const queryList = [];
|
const suggestedList = [];
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result.room_id) {
|
if (result.room_id) {
|
||||||
queryList.push({
|
suggestedList.push({
|
||||||
addressType: 'mx-room-id',
|
addressType: 'mx-room-id',
|
||||||
address: result.room_id,
|
address: result.room_id,
|
||||||
displayName: result.name,
|
displayName: result.name,
|
||||||
|
@ -399,7 +405,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// Return objects, structure of which is defined
|
// Return objects, structure of which is defined
|
||||||
// by UserAddressType
|
// by UserAddressType
|
||||||
queryList.push({
|
suggestedList.push({
|
||||||
addressType: 'mx-user-id',
|
addressType: 'mx-user-id',
|
||||||
address: result.user_id,
|
address: result.user_id,
|
||||||
displayName: result.display_name,
|
displayName: result.display_name,
|
||||||
|
@ -413,18 +419,18 @@ module.exports = React.createClass({
|
||||||
// a perfectly valid address if there are close matches.
|
// a perfectly valid address if there are close matches.
|
||||||
const addrType = getAddressType(query);
|
const addrType = getAddressType(query);
|
||||||
if (this.props.validAddressTypes.includes(addrType)) {
|
if (this.props.validAddressTypes.includes(addrType)) {
|
||||||
queryList.unshift({
|
suggestedList.unshift({
|
||||||
addressType: addrType,
|
addressType: addrType,
|
||||||
address: query,
|
address: query,
|
||||||
isKnown: false,
|
isKnown: false,
|
||||||
});
|
});
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
if (addrType == 'email') {
|
if (addrType === 'email') {
|
||||||
this._lookupThreepid(addrType, query).done();
|
this._lookupThreepid(addrType, query).done();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
queryList,
|
suggestedList,
|
||||||
error: false,
|
error: false,
|
||||||
}, () => {
|
}, () => {
|
||||||
if (this.addressSelector) this.addressSelector.moveSelectionTop();
|
if (this.addressSelector) this.addressSelector.moveSelectionTop();
|
||||||
|
@ -442,14 +448,14 @@ module.exports = React.createClass({
|
||||||
if (!this.props.validAddressTypes.includes(addrType)) {
|
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||||
this.setState({ error: true });
|
this.setState({ error: true });
|
||||||
return null;
|
return null;
|
||||||
} else if (addrType == 'mx-user-id') {
|
} else if (addrType === 'mx-user-id') {
|
||||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||||
if (user) {
|
if (user) {
|
||||||
addrObj.displayName = user.displayName;
|
addrObj.displayName = user.displayName;
|
||||||
addrObj.avatarMxc = user.avatarUrl;
|
addrObj.avatarMxc = user.avatarUrl;
|
||||||
addrObj.isKnown = true;
|
addrObj.isKnown = true;
|
||||||
}
|
}
|
||||||
} else if (addrType == 'mx-room-id') {
|
} else if (addrType === 'mx-room-id') {
|
||||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||||
if (room) {
|
if (room) {
|
||||||
addrObj.displayName = room.name;
|
addrObj.displayName = room.name;
|
||||||
|
@ -458,15 +464,15 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userList = this.state.userList.slice();
|
const selectedList = this.state.selectedList.slice();
|
||||||
userList.push(addrObj);
|
selectedList.push(addrObj);
|
||||||
this.setState({
|
this.setState({
|
||||||
userList: userList,
|
selectedList,
|
||||||
queryList: [],
|
suggestedList: [],
|
||||||
query: "",
|
query: "",
|
||||||
});
|
});
|
||||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
return userList;
|
return selectedList;
|
||||||
},
|
},
|
||||||
|
|
||||||
_lookupThreepid: function(medium, address) {
|
_lookupThreepid: function(medium, address) {
|
||||||
|
@ -492,7 +498,7 @@ module.exports = React.createClass({
|
||||||
if (res === null) return null;
|
if (res === null) return null;
|
||||||
if (cancelled) return null;
|
if (cancelled) return null;
|
||||||
this.setState({
|
this.setState({
|
||||||
queryList: [{
|
suggestedList: [{
|
||||||
// a UserAddressType
|
// a UserAddressType
|
||||||
addressType: medium,
|
addressType: medium,
|
||||||
address: address,
|
address: address,
|
||||||
|
@ -510,15 +516,27 @@ module.exports = React.createClass({
|
||||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||||
this.scrollElement = null;
|
this.scrollElement = null;
|
||||||
|
|
||||||
|
// map addressType => set of addresses to avoid O(n*m) operation
|
||||||
|
const selectedAddresses = {};
|
||||||
|
this.state.selectedList.forEach(({address, addressType}) => {
|
||||||
|
if (!selectedAddresses[addressType]) selectedAddresses[addressType] = new Set();
|
||||||
|
selectedAddresses[addressType].add(address);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter out any addresses in the above already selected addresses (matching both type and address)
|
||||||
|
const filteredSuggestedList = this.state.suggestedList.filter(({address, addressType}) => {
|
||||||
|
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
|
||||||
|
});
|
||||||
|
|
||||||
const query = [];
|
const query = [];
|
||||||
// create the invite list
|
// create the invite list
|
||||||
if (this.state.userList.length > 0) {
|
if (this.state.selectedList.length > 0) {
|
||||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||||
for (let i = 0; i < this.state.userList.length; i++) {
|
for (let i = 0; i < this.state.selectedList.length; i++) {
|
||||||
query.push(
|
query.push(
|
||||||
<AddressTile
|
<AddressTile
|
||||||
key={i}
|
key={i}
|
||||||
address={this.state.userList[i]}
|
address={this.state.selectedList[i]}
|
||||||
canDismiss={true}
|
canDismiss={true}
|
||||||
onDismissed={this.onDismissed(i)}
|
onDismissed={this.onDismissed(i)}
|
||||||
showAddress={this.props.pickerType === 'user'} />,
|
showAddress={this.props.pickerType === 'user'} />,
|
||||||
|
@ -528,7 +546,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// Add the query at the end
|
// Add the query at the end
|
||||||
query.push(
|
query.push(
|
||||||
<textarea key={this.state.userList.length}
|
<textarea key={this.state.selectedList.length}
|
||||||
rows="1"
|
rows="1"
|
||||||
id="textinput"
|
id="textinput"
|
||||||
ref="textinput"
|
ref="textinput"
|
||||||
|
@ -543,34 +561,22 @@ module.exports = React.createClass({
|
||||||
let error;
|
let error;
|
||||||
let addressSelector;
|
let addressSelector;
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
let tryUsing = '';
|
const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t]));
|
||||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => {
|
|
||||||
return {
|
|
||||||
'mx-user-id': _t("Matrix ID"),
|
|
||||||
'mx-room-id': _t("Matrix Room ID"),
|
|
||||||
'email': _t("email address"),
|
|
||||||
}[t];
|
|
||||||
});
|
|
||||||
tryUsing = _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
|
||||||
validTypesList: validTypeDescriptions.join(", "),
|
|
||||||
});
|
|
||||||
error = <div className="mx_ChatInviteDialog_error">
|
error = <div className="mx_ChatInviteDialog_error">
|
||||||
{ _t("You have entered an invalid address.") }
|
{ _t("You have entered an invalid address.") }
|
||||||
<br />
|
<br />
|
||||||
{ tryUsing }
|
{ _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||||
|
validTypesList: validTypeDescriptions.join(", "),
|
||||||
|
}) }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.searchError) {
|
} else if (this.state.searchError) {
|
||||||
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
|
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
|
||||||
} else if (
|
} else if (this.state.query.length > 0 && filteredSuggestedList.length === 0 && !this.state.busy) {
|
||||||
this.state.query.length > 0 &&
|
|
||||||
this.state.queryList.length === 0 &&
|
|
||||||
!this.state.busy
|
|
||||||
) {
|
|
||||||
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
||||||
} else {
|
} else {
|
||||||
addressSelector = (
|
addressSelector = (
|
||||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||||
addressList={this.state.queryList}
|
addressList={filteredSuggestedList}
|
||||||
showAddress={this.props.pickerType === 'user'}
|
showAddress={this.props.pickerType === 'user'}
|
||||||
onSelected={this.onSelected}
|
onSelected={this.onSelected}
|
||||||
truncateAt={TRUNCATE_QUERY_LIST}
|
truncateAt={TRUNCATE_QUERY_LIST}
|
||||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
|
||||||
|
@ -64,7 +65,10 @@ export default React.createClass({
|
||||||
|
|
||||||
// Id of content element
|
// Id of content element
|
||||||
// If provided, this is used to add a aria-describedby attribute
|
// If provided, this is used to add a aria-describedby attribute
|
||||||
contentId: React.PropTypes.string,
|
contentId: PropTypes.string,
|
||||||
|
|
||||||
|
// optional additional class for the title element
|
||||||
|
titleClass: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -105,25 +109,28 @@ export default React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
|
let cancelButton;
|
||||||
|
if (this.props.hasCancel) {
|
||||||
|
cancelButton = <AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton">
|
||||||
|
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusTrap onKeyDown={this._onKeyDown}
|
<FocusTrap onKeyDown={this._onKeyDown}
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-labelledby='mx_BaseDialog_title'
|
aria-labelledby='mx_BaseDialog_title'
|
||||||
// This should point to a node describing the dialog.
|
// This should point to a node describing the dialog.
|
||||||
// If we were about to completelly follow this recommendation we'd need to
|
// If we were about to completely follow this recommendation we'd need to
|
||||||
// make all the components relying on BaseDialog to be aware of it.
|
// make all the components relying on BaseDialog to be aware of it.
|
||||||
// So instead we will use the whole content as the description.
|
// So instead we will use the whole content as the description.
|
||||||
// Description comes first and if the content contains more text,
|
// Description comes first and if the content contains more text,
|
||||||
// AT users can skip its presentation.
|
// AT users can skip its presentation.
|
||||||
aria-describedby={this.props.contentId}
|
aria-describedby={this.props.contentId}
|
||||||
>
|
>
|
||||||
{ this.props.hasCancel ? <AccessibleButton onClick={this._onCancelClick}
|
{ cancelButton }
|
||||||
className="mx_Dialog_cancelButton"
|
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||||
>
|
|
||||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
|
||||||
</AccessibleButton> : null }
|
|
||||||
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
|
|
||||||
{ this.props.title }
|
{ this.props.title }
|
||||||
</div>
|
</div>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -28,6 +29,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.onFinished = this.onFinished.bind(this);
|
||||||
this.onRoomTileClick = this.onRoomTileClick.bind(this);
|
this.onRoomTileClick = this.onRoomTileClick.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -53,10 +55,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
if (room) {
|
if (room) {
|
||||||
const me = room.getMember(client.credentials.userId);
|
const me = room.getMember(client.credentials.userId);
|
||||||
const highlight = (
|
const highlight = room.getUnreadNotificationCount('highlight') > 0 || me.membership === "invite";
|
||||||
room.getUnreadNotificationCount('highlight') > 0 ||
|
|
||||||
me.membership == "invite"
|
|
||||||
);
|
|
||||||
tiles.push(
|
tiles.push(
|
||||||
<RoomTile key={room.roomId} room={room}
|
<RoomTile key={room.roomId} room={room}
|
||||||
transparent={true}
|
transparent={true}
|
||||||
|
@ -64,7 +63,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
selected={false}
|
selected={false}
|
||||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||||
highlight={highlight}
|
highlight={highlight}
|
||||||
isInvite={me.membership == "invite"}
|
isInvite={me.membership === "invite"}
|
||||||
onClick={this.onRoomTileClick}
|
onClick={this.onRoomTileClick}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
@ -110,6 +109,10 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
this.props.onExistingRoomSelected(roomId);
|
this.props.onExistingRoomSelected(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFinished() {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let title = '';
|
let title = '';
|
||||||
let content = null;
|
let content = null;
|
||||||
|
@ -170,14 +173,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
{ profile }
|
{ profile }
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons primaryButton={_t('Start Chatting')}
|
<DialogButtons primaryButton={_t('Start Chatting')}
|
||||||
onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
|
onPrimaryButtonClick={this.props.onNewDMClick} focus={true} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||||
onFinished={this.props.onFinished.bind(false)}
|
onFinished={this.onFinished}
|
||||||
title={title}
|
title={title}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
|
@ -187,7 +190,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatCreateOrReuseDialog.propTyps = {
|
ChatCreateOrReuseDialog.propTypes = {
|
||||||
userId: PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
// Called when clicking outside of the dialog
|
// Called when clicking outside of the dialog
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -52,8 +52,8 @@ export default React.createClass({
|
||||||
<div className="mx_CreateRoomDialog_label">
|
<div className="mx_CreateRoomDialog_label">
|
||||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="mx_CreateRoomDialog_input_container">
|
||||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} />
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
|
@ -33,10 +33,21 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
this._onOk = this._onOk.bind(this);
|
this._onOk = this._onOk.bind(this);
|
||||||
this._onCancel = this._onCancel.bind(this);
|
this._onCancel = this._onCancel.bind(this);
|
||||||
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
||||||
|
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||||
|
|
||||||
|
const deactivationPreferences =
|
||||||
|
MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences');
|
||||||
|
|
||||||
|
const shouldErase = (
|
||||||
|
deactivationPreferences &&
|
||||||
|
deactivationPreferences.getContent() &&
|
||||||
|
deactivationPreferences.getContent().shouldErase
|
||||||
|
) || false;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
confirmButtonEnabled: false,
|
confirmButtonEnabled: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
|
shouldErase,
|
||||||
errStr: null,
|
errStr: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -47,19 +58,55 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onOk() {
|
_onEraseFieldChange(ev) {
|
||||||
|
this.setState({
|
||||||
|
shouldErase: ev.target.checked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onOk() {
|
||||||
|
this.setState({busy: true});
|
||||||
|
|
||||||
|
// Before we deactivate the account insert an event into
|
||||||
|
// the user's account data indicating that they wish to be
|
||||||
|
// erased from the homeserver.
|
||||||
|
//
|
||||||
|
// We do this because the API for erasing after deactivation
|
||||||
|
// might not be supported by the connected homeserver. Leaving
|
||||||
|
// an indication in account data is only best-effort, and
|
||||||
|
// in the worse case, the HS maintainer would have to run a
|
||||||
|
// script to erase deactivated accounts that have shouldErase
|
||||||
|
// set to true in im.riot.account_deactivation_preferences.
|
||||||
|
//
|
||||||
|
// Note: The preferences are scoped to Riot, hence the
|
||||||
|
// "im.riot..." event type.
|
||||||
|
//
|
||||||
|
// Note: This may have already been set on previous attempts
|
||||||
|
// where, for example, the user entered the wrong password.
|
||||||
|
// This is fine because the UI always indicates the preference
|
||||||
|
// prior to us calling `deactivateAccount`.
|
||||||
|
try {
|
||||||
|
await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', {
|
||||||
|
shouldErase: this.state.shouldErase,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({
|
||||||
|
busy: false,
|
||||||
|
errStr: _t('Failed to indicate account erasure'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// This assumes that the HS requires password UI auth
|
// This assumes that the HS requires password UI auth
|
||||||
// for this endpoint. In reality it could be any UI auth.
|
// for this endpoint. In reality it could be any UI auth.
|
||||||
this.setState({busy: true});
|
const auth = {
|
||||||
MatrixClientPeg.get().deactivateAccount({
|
|
||||||
type: 'm.login.password',
|
type: 'm.login.password',
|
||||||
user: MatrixClientPeg.get().credentials.userId,
|
user: MatrixClientPeg.get().credentials.userId,
|
||||||
password: this._passwordField.value,
|
password: this._passwordField.value,
|
||||||
}).done(() => {
|
};
|
||||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
|
||||||
Lifecycle.onLoggedOut();
|
} catch (err) {
|
||||||
this.props.onFinished(false);
|
|
||||||
}, (err) => {
|
|
||||||
let errStr = _t('Unknown error');
|
let errStr = _t('Unknown error');
|
||||||
// https://matrix.org/jira/browse/SYN-744
|
// https://matrix.org/jira/browse/SYN-744
|
||||||
if (err.httpStatus == 401 || err.httpStatus == 403) {
|
if (err.httpStatus == 401 || err.httpStatus == 403) {
|
||||||
|
@ -70,7 +117,12 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
busy: false,
|
busy: false,
|
||||||
errStr: errStr,
|
errStr: errStr,
|
||||||
});
|
});
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||||
|
Lifecycle.onLoggedOut();
|
||||||
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCancel() {
|
_onCancel() {
|
||||||
|
@ -105,21 +157,64 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this.onOk}
|
onEnterPressed={this.onOk}
|
||||||
titleClass="danger"
|
titleClass="danger"
|
||||||
title={_t("Deactivate Account")}>
|
title={_t("Deactivate Account")}
|
||||||
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
|
<p>{ _t(
|
||||||
|
"This will make your account permanently unusable. " +
|
||||||
|
"You will not be able to log in, and no one will be able to re-register the same " +
|
||||||
|
"user ID. " +
|
||||||
|
"This will cause your account to leave all rooms it is participating in, and it " +
|
||||||
|
"will remove your account details from your identity server. " +
|
||||||
|
"<b>This action is irreversible.</b>",
|
||||||
|
{},
|
||||||
|
{ b: (sub) => <b> { sub } </b> },
|
||||||
|
) }</p>
|
||||||
|
|
||||||
<p>{ _t("This action is irreversible.") }</p>
|
<p>{ _t(
|
||||||
|
"Deactivating your account <b>does not by default cause us to forget messages you " +
|
||||||
|
"have sent.</b> " +
|
||||||
|
"If you would like us to forget your messages, please tick the box below.",
|
||||||
|
{},
|
||||||
|
{ b: (sub) => <b> { sub } </b> },
|
||||||
|
) }</p>
|
||||||
|
|
||||||
<p>{ _t("To continue, please enter your password.") }</p>
|
<p>{ _t(
|
||||||
|
"Message visibility in Matrix is similar to email. " +
|
||||||
|
"Our forgetting your messages means that messages you have sent will not be shared " +
|
||||||
|
"with any new or unregistered users, but registered users who already have access " +
|
||||||
|
"to these messages will still have access to their copy.",
|
||||||
|
) }</p>
|
||||||
|
|
||||||
<p>{ _t("Password") }:</p>
|
<div className="mx_DeactivateAccountDialog_input_section">
|
||||||
|
<p>
|
||||||
|
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input">
|
||||||
|
<input
|
||||||
|
id="mx_DeactivateAccountDialog_erase_account_input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.shouldErase}
|
||||||
|
onChange={this._onEraseFieldChange}
|
||||||
|
/>
|
||||||
|
{ _t(
|
||||||
|
"Please forget all messages I have sent when my account is deactivated " +
|
||||||
|
"(<b>Warning:</b> this will cause future users to see an incomplete view " +
|
||||||
|
"of conversations)",
|
||||||
|
{},
|
||||||
|
{ b: (sub) => <b>{ sub }</b> },
|
||||||
|
) }
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>{ _t("To continue, please enter your password:") }</p>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
|
placeholder={_t("password")}
|
||||||
onChange={this._onPasswordFieldChange}
|
onChange={this._onPasswordFieldChange}
|
||||||
ref={(e) => {this._passwordField = e;}}
|
ref={(e) => {this._passwordField = e;}}
|
||||||
className={passwordBoxClass}
|
className={passwordBoxClass}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{ error }
|
{ error }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
|
|
|
@ -132,17 +132,17 @@ class SendCustomEvent extends GenericEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_DevTools_content">
|
||||||
{ this.textInput('eventType', _t('Event Type')) }
|
{ this.textInput('eventType', _t('Event Type')) }
|
||||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_DevTools_inputLabelCell">
|
||||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
|
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
|
@ -219,15 +219,15 @@ class SendAccountData extends GenericEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_DevTools_content">
|
||||||
{ this.textInput('eventType', _t('Event Type')) }
|
{ this.textInput('eventType', _t('Event Type')) }
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_DevTools_inputLabelCell">
|
||||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
|
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
|
@ -242,6 +242,9 @@ class SendAccountData extends GenericEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const INITIAL_LOAD_TILES = 20;
|
||||||
|
const LOAD_TILES_STEP_SIZE = 50;
|
||||||
|
|
||||||
class FilteredList extends React.Component {
|
class FilteredList extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
|
@ -249,31 +252,65 @@ class FilteredList extends React.Component {
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static filterChildren(children, query) {
|
||||||
|
if (!query) return children;
|
||||||
|
const lcQuery = query.toLowerCase();
|
||||||
|
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.onQuery = this.onQuery.bind(this);
|
|
||||||
|
this.state = {
|
||||||
|
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
|
||||||
|
truncateAt: INITIAL_LOAD_TILES,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onQuery(ev) {
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
||||||
|
this.setState({
|
||||||
|
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
||||||
|
truncateAt: INITIAL_LOAD_TILES,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showAll = () => {
|
||||||
|
this.setState({
|
||||||
|
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||||
|
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
||||||
|
{ _t("and %(count)s others...", { count: overflowCount }) }
|
||||||
|
</button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
onQuery = (ev) => {
|
||||||
if (this.props.onChange) this.props.onChange(ev.target.value);
|
if (this.props.onChange) this.props.onChange(ev.target.value);
|
||||||
}
|
};
|
||||||
|
|
||||||
filterChildren() {
|
getChildren = (start: number, end: number) => {
|
||||||
if (this.props.query) {
|
return this.state.filteredChildren.slice(start, end);
|
||||||
const lowerQuery = this.props.query.toLowerCase();
|
};
|
||||||
return this.props.children.filter((child) => child.key.toLowerCase().includes(lowerQuery));
|
|
||||||
}
|
getChildCount = (): number => {
|
||||||
return this.props.children;
|
return this.state.filteredChildren.length;
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||||
return <div>
|
return <div>
|
||||||
<input size="64"
|
<input size="64"
|
||||||
onChange={this.onQuery}
|
onChange={this.onQuery}
|
||||||
value={this.props.query}
|
value={this.props.query}
|
||||||
placeholder={_t('Filter results')}
|
placeholder={_t('Filter results')}
|
||||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
|
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
|
||||||
{ this.filterChildren() }
|
<TruncatedList getChildren={this.getChildren}
|
||||||
|
getChildCount={this.getChildCount}
|
||||||
|
truncateAt={this.state.truncateAt}
|
||||||
|
createOverflowElement={this.createOverflowElement} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -485,7 +522,7 @@ class AccountDataExplorer extends DevtoolsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_ViewSource">
|
return <div className="mx_ViewSource">
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_DevTools_content">
|
||||||
<SyntaxHighlight className="json">
|
<SyntaxHighlight className="json">
|
||||||
{ JSON.stringify(this.state.event.event, null, 2) }
|
{ JSON.stringify(this.state.event.event, null, 2) }
|
||||||
</SyntaxHighlight>
|
</SyntaxHighlight>
|
||||||
|
|
|
@ -67,8 +67,10 @@ export default React.createClass({
|
||||||
{ this.props.description }
|
{ this.props.description }
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||||
onPrimaryButtonClick={this.onOk}
|
|
||||||
primaryButtonClass={primaryButtonClass}
|
primaryButtonClass={primaryButtonClass}
|
||||||
|
cancelButton={this.props.cancelButton}
|
||||||
|
hasCancel={this.props.hasCancelButton}
|
||||||
|
onPrimaryButtonClick={this.onOk}
|
||||||
focus={this.props.focus}
|
focus={this.props.focus}
|
||||||
onCancel={this.onCancel}
|
onCancel={this.onCancel}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -36,7 +37,7 @@ export default React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
emailAddress: null,
|
emailAddress: '',
|
||||||
emailBusy: false,
|
emailBusy: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -127,6 +128,7 @@ export default React.createClass({
|
||||||
const EditableText = sdk.getComponent('elements.EditableText');
|
const EditableText = sdk.getComponent('elements.EditableText');
|
||||||
|
|
||||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||||
|
initialValue={this.state.emailAddress}
|
||||||
className="mx_SetEmailDialog_email_input"
|
className="mx_SetEmailDialog_email_input"
|
||||||
autoFocus="true"
|
autoFocus="true"
|
||||||
placeholder={_t("Email address")}
|
placeholder={_t("Email address")}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -79,15 +80,11 @@ export default React.createClass({
|
||||||
Modal.createDialog(WarmFuzzy, {
|
Modal.createDialog(WarmFuzzy, {
|
||||||
didSetEmail: res.didSetEmail,
|
didSetEmail: res.didSetEmail,
|
||||||
onFinished: () => {
|
onFinished: () => {
|
||||||
this._onContinueClicked();
|
this.props.onFinished();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onContinueClicked: function() {
|
|
||||||
this.props.onFinished(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
_onPasswordChangeError: function(err) {
|
_onPasswordChangeError: function(err) {
|
||||||
let errMsg = err.error || "";
|
let errMsg = err.error || "";
|
||||||
if (err.httpStatus === 403) {
|
if (err.httpStatus === 403) {
|
||||||
|
|
224
src/components/views/dialogs/ShareDialog.js
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import QRCode from 'qrcode-react';
|
||||||
|
import {makeEventPermalink, makeGroupPermalink, makeRoomPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||||
|
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||||
|
|
||||||
|
const socials = [
|
||||||
|
{
|
||||||
|
name: 'Facebook',
|
||||||
|
img: 'img/social/facebook.png',
|
||||||
|
url: (url) => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
|
||||||
|
}, {
|
||||||
|
name: 'Twitter',
|
||||||
|
img: 'img/social/twitter-2.png',
|
||||||
|
url: (url) => `https://twitter.com/home?status=${url}`,
|
||||||
|
}, /* // icon missing
|
||||||
|
name: 'Google Plus',
|
||||||
|
img: 'img/social/',
|
||||||
|
url: (url) => `https://plus.google.com/share?url=${url}`,
|
||||||
|
},*/ {
|
||||||
|
name: 'LinkedIn',
|
||||||
|
img: 'img/social/linkedin.png',
|
||||||
|
url: (url) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`,
|
||||||
|
}, {
|
||||||
|
name: 'Reddit',
|
||||||
|
img: 'img/social/reddit.png',
|
||||||
|
url: (url) => `http://www.reddit.com/submit?url=${url}`,
|
||||||
|
}, {
|
||||||
|
name: 'email',
|
||||||
|
img: 'img/social/email-1.png',
|
||||||
|
url: (url) => `mailto:?body=${url}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default class ShareDialog extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
target: PropTypes.oneOfType([
|
||||||
|
PropTypes.instanceOf(Room),
|
||||||
|
PropTypes.instanceOf(User),
|
||||||
|
PropTypes.instanceOf(Group),
|
||||||
|
PropTypes.instanceOf(RoomMember),
|
||||||
|
PropTypes.instanceOf(MatrixEvent),
|
||||||
|
]).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.onCopyClick = this.onCopyClick.bind(this);
|
||||||
|
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
// MatrixEvent defaults to share linkSpecificEvent
|
||||||
|
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static _selectText(target) {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(target);
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
static onLinkClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const {target} = e;
|
||||||
|
ShareDialog._selectText(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCopyClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
ShareDialog._selectText(this.refs.link);
|
||||||
|
|
||||||
|
let successful;
|
||||||
|
try {
|
||||||
|
successful = document.execCommand('copy');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||||
|
const buttonRect = e.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||||
|
const x = buttonRect.right + window.pageXOffset;
|
||||||
|
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||||
|
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
|
||||||
|
chevronOffset: 10,
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||||
|
}, false);
|
||||||
|
e.target.onmouseleave = close;
|
||||||
|
}
|
||||||
|
|
||||||
|
onLinkSpecificEventCheckboxClick() {
|
||||||
|
this.setState({
|
||||||
|
linkSpecificEvent: !this.state.linkSpecificEvent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let title;
|
||||||
|
let matrixToUrl;
|
||||||
|
|
||||||
|
let checkbox;
|
||||||
|
|
||||||
|
if (this.props.target instanceof Room) {
|
||||||
|
title = _t('Share Room');
|
||||||
|
|
||||||
|
const events = this.props.target.getLiveTimeline().getEvents();
|
||||||
|
if (events.length > 0) {
|
||||||
|
checkbox = <div>
|
||||||
|
<input type="checkbox"
|
||||||
|
id="mx_ShareDialog_checkbox"
|
||||||
|
checked={this.state.linkSpecificEvent}
|
||||||
|
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||||
|
<label htmlFor="mx_ShareDialog_checkbox">
|
||||||
|
{ _t('Link to most recent message') }
|
||||||
|
</label>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.linkSpecificEvent) {
|
||||||
|
matrixToUrl = makeEventPermalink(this.props.target.roomId, events[events.length - 1].getId());
|
||||||
|
} else {
|
||||||
|
matrixToUrl = makeRoomPermalink(this.props.target.roomId);
|
||||||
|
}
|
||||||
|
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||||
|
title = _t('Share User');
|
||||||
|
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||||
|
} else if (this.props.target instanceof Group) {
|
||||||
|
title = _t('Share Community');
|
||||||
|
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
||||||
|
} else if (this.props.target instanceof MatrixEvent) {
|
||||||
|
title = _t('Share Room Message');
|
||||||
|
checkbox = <div>
|
||||||
|
<input type="checkbox"
|
||||||
|
id="mx_ShareDialog_checkbox"
|
||||||
|
checked={this.state.linkSpecificEvent}
|
||||||
|
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||||
|
<label htmlFor="mx_ShareDialog_checkbox">
|
||||||
|
{ _t('Link to selected message') }
|
||||||
|
</label>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
if (this.state.linkSpecificEvent) {
|
||||||
|
matrixToUrl = makeEventPermalink(this.props.target.getRoomId(), this.props.target.getId());
|
||||||
|
} else {
|
||||||
|
matrixToUrl = makeRoomPermalink(this.props.target.getRoomId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedUrl = encodeURIComponent(matrixToUrl);
|
||||||
|
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
return <BaseDialog title={title}
|
||||||
|
className='mx_ShareDialog'
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
>
|
||||||
|
<div className="mx_ShareDialog_content">
|
||||||
|
<div className="mx_ShareDialog_matrixto">
|
||||||
|
<a ref="link"
|
||||||
|
href={matrixToUrl}
|
||||||
|
onClick={ShareDialog.onLinkClick}
|
||||||
|
className="mx_ShareDialog_matrixto_link"
|
||||||
|
>
|
||||||
|
{ matrixToUrl }
|
||||||
|
</a>
|
||||||
|
<a href={matrixToUrl} className="mx_ShareDialog_matrixto_copy" onClick={this.onCopyClick}>
|
||||||
|
{ _t('COPY') }
|
||||||
|
<div> </div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{ checkbox }
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div className="mx_ShareDialog_split">
|
||||||
|
<div className="mx_ShareDialog_qrcode_container">
|
||||||
|
<QRCode value={matrixToUrl} size={256} logoWidth={48} logo="img/matrix-m.svg" />
|
||||||
|
</div>
|
||||||
|
<div className="mx_ShareDialog_social_container">
|
||||||
|
{
|
||||||
|
socials.map((social) => <a rel="noopener"
|
||||||
|
target="_blank"
|
||||||
|
key={social.name}
|
||||||
|
name={social.name}
|
||||||
|
href={social.url(encodedUrl)}
|
||||||
|
className="mx_ShareDialog_social_icon"
|
||||||
|
>
|
||||||
|
<img src={social.img} alt={social.name} height={64} width={64} />
|
||||||
|
</a>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||||
|
|
||||||
export default class AppPermission extends React.Component {
|
export default class AppPermission extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -19,7 +20,7 @@ export default class AppPermission extends React.Component {
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(wurl.search);
|
const searchParams = new URLSearchParams(wurl.search);
|
||||||
|
|
||||||
if (this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) {
|
||||||
curl = url.parse(searchParams.get('url'));
|
curl = url.parse(searchParams.get('url'));
|
||||||
if (curl) {
|
if (curl) {
|
||||||
curl.search = curl.query = "";
|
curl.search = curl.query = "";
|
||||||
|
@ -33,25 +34,16 @@ export default class AppPermission extends React.Component {
|
||||||
return curlString;
|
return curlString;
|
||||||
}
|
}
|
||||||
|
|
||||||
isScalarWurl(wurl) {
|
|
||||||
if (wurl && wurl.hostname && (
|
|
||||||
wurl.hostname === 'scalar.vector.im' ||
|
|
||||||
wurl.hostname === 'scalar-staging.riot.im' ||
|
|
||||||
wurl.hostname === 'scalar-develop.riot.im' ||
|
|
||||||
wurl.hostname === 'demo.riot.im' ||
|
|
||||||
wurl.hostname === 'localhost'
|
|
||||||
)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let e2eWarningText;
|
let e2eWarningText;
|
||||||
if (this.props.isRoomEncrypted) {
|
if (this.props.isRoomEncrypted) {
|
||||||
e2eWarningText =
|
e2eWarningText =
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
|
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
|
||||||
}
|
}
|
||||||
|
const cookieWarning =
|
||||||
|
<span className='mx_AppPermissionWarningTextLabel'>
|
||||||
|
{ _t('Warning: This widget might use cookies.') }
|
||||||
|
</span>;
|
||||||
return (
|
return (
|
||||||
<div className='mx_AppPermissionWarning'>
|
<div className='mx_AppPermissionWarning'>
|
||||||
<div className='mx_AppPermissionWarningImage'>
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
|
@ -60,6 +52,7 @@ export default class AppPermission extends React.Component {
|
||||||
<div className='mx_AppPermissionWarningText'>
|
<div className='mx_AppPermissionWarningText'>
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
|
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
|
||||||
{ e2eWarningText }
|
{ e2eWarningText }
|
||||||
|
{ cookieWarning }
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
className='mx_AppPermissionButton'
|
className='mx_AppPermissionButton'
|
||||||
|
|
|
@ -25,14 +25,13 @@ import PlatformPeg from '../../../PlatformPeg';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import WidgetMessaging from '../../../WidgetMessaging';
|
import WidgetMessaging from '../../../WidgetMessaging';
|
||||||
import TintableSvgButton from './TintableSvgButton';
|
import TintableSvgButton from './TintableSvgButton';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import AppPermission from './AppPermission';
|
import AppPermission from './AppPermission';
|
||||||
import AppWarning from './AppWarning';
|
import AppWarning from './AppWarning';
|
||||||
import MessageSpinner from './MessageSpinner';
|
import MessageSpinner from './MessageSpinner';
|
||||||
import WidgetUtils from '../../../WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||||
|
@ -55,6 +54,7 @@ export default class AppTile extends React.Component {
|
||||||
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
||||||
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
||||||
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
|
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
|
||||||
|
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,30 +120,6 @@ export default class AppTile extends React.Component {
|
||||||
return u.format();
|
return u.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
|
||||||
* @param {[type]} url URL to check
|
|
||||||
* @return {Boolean} True if specified URL is a scalar URL
|
|
||||||
*/
|
|
||||||
isScalarUrl(url) {
|
|
||||||
if (!url) {
|
|
||||||
console.error('Scalar URL check failed. No URL specified');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
|
|
||||||
if (!scalarUrls || scalarUrls.length == 0) {
|
|
||||||
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < scalarUrls.length; i++) {
|
|
||||||
if (url.startsWith(scalarUrls[i])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
isMixedContent() {
|
isMixedContent() {
|
||||||
const parentContentProtocol = window.location.protocol;
|
const parentContentProtocol = window.location.protocol;
|
||||||
const u = url.parse(this.props.url);
|
const u = url.parse(this.props.url);
|
||||||
|
@ -199,7 +175,7 @@ export default class AppTile extends React.Component {
|
||||||
setScalarToken() {
|
setScalarToken() {
|
||||||
this.setState({initialising: true});
|
this.setState({initialising: true});
|
||||||
|
|
||||||
if (!this.isScalarUrl(this.props.url)) {
|
if (!WidgetUtils.isScalarUrl(this.props.url)) {
|
||||||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||||
this.setState({
|
this.setState({
|
||||||
error: null,
|
error: null,
|
||||||
|
@ -269,7 +245,12 @@ export default class AppTile extends React.Component {
|
||||||
event.origin = event.originalEvent.origin;
|
event.origin = event.originalEvent.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.widgetUrl.startsWith(event.origin)) {
|
const widgetUrlObj = url.parse(this.state.widgetUrl);
|
||||||
|
const eventOrigin = url.parse(event.origin);
|
||||||
|
if (
|
||||||
|
eventOrigin.protocol !== widgetUrlObj.protocol ||
|
||||||
|
eventOrigin.host !== widgetUrlObj.host
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,10 +319,9 @@ export default class AppTile extends React.Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({deleting: true});
|
this.setState({deleting: true});
|
||||||
MatrixClientPeg.get().sendStateEvent(
|
|
||||||
|
WidgetUtils.setRoomWidget(
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
'im.vector.modular.widgets',
|
|
||||||
{}, // empty content
|
|
||||||
this.props.id,
|
this.props.id,
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
console.error('Failed to delete widget', e);
|
console.error('Failed to delete widget', e);
|
||||||
|
@ -519,6 +499,11 @@ export default class AppTile extends React.Component {
|
||||||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener noreferrer'}).click();
|
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener noreferrer'}).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onReloadWidgetClick(e) {
|
||||||
|
// Reload iframe in this way to avoid cross-origin restrictions
|
||||||
|
this.refs.appFrame.src = this.refs.appFrame.src;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
|
||||||
|
@ -606,6 +591,7 @@ export default class AppTile extends React.Component {
|
||||||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
||||||
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
||||||
const popoutWidgetIcon = 'img/button-new-window.svg';
|
const popoutWidgetIcon = 'img/button-new-window.svg';
|
||||||
|
const reloadWidgetIcon = 'img/button-refresh.svg';
|
||||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -624,6 +610,16 @@ export default class AppTile extends React.Component {
|
||||||
{ this.props.showTitle && this._getTileTitle() }
|
{ this.props.showTitle && this._getTileTitle() }
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
|
{ /* Reload widget */ }
|
||||||
|
{ this.props.showReload && <TintableSvgButton
|
||||||
|
src={reloadWidgetIcon}
|
||||||
|
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
|
title={_t('Reload widget')}
|
||||||
|
onClick={this._onReloadWidgetClick}
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
/> }
|
||||||
|
|
||||||
{ /* Popout widget */ }
|
{ /* Popout widget */ }
|
||||||
{ this.props.showPopout && <TintableSvgButton
|
{ this.props.showPopout && <TintableSvgButton
|
||||||
src={popoutWidgetIcon}
|
src={popoutWidgetIcon}
|
||||||
|
@ -707,6 +703,11 @@ AppTile.propTypes = {
|
||||||
showDelete: PropTypes.bool,
|
showDelete: PropTypes.bool,
|
||||||
// Optionally hide the popout widget icon
|
// Optionally hide the popout widget icon
|
||||||
showPopout: PropTypes.bool,
|
showPopout: PropTypes.bool,
|
||||||
|
// Optionally show the reload widget icon
|
||||||
|
// This is not currently intended for use with production widgets. However
|
||||||
|
// it can be useful when developing persistent widgets in order to avoid
|
||||||
|
// having to reload all of riot to get new widget content.
|
||||||
|
showReload: PropTypes.bool,
|
||||||
// Widget capabilities to allow by default (without user confirmation)
|
// Widget capabilities to allow by default (without user confirmation)
|
||||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||||
// basic widget capabilities, e.g. injecting sticker message events.
|
// basic widget capabilities, e.g. injecting sticker message events.
|
||||||
|
@ -726,6 +727,7 @@ AppTile.defaultProps = {
|
||||||
showMinimise: true,
|
showMinimise: true,
|
||||||
showDelete: true,
|
showDelete: true,
|
||||||
showPopout: true,
|
showPopout: true,
|
||||||
|
showReload: false,
|
||||||
handleMinimisePointerEvents: false,
|
handleMinimisePointerEvents: false,
|
||||||
whitelistCapabilities: [],
|
whitelistCapabilities: [],
|
||||||
userWidget: false,
|
userWidget: false,
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import TagTile from './TagTile';
|
import TagTile from './TagTile';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import { Draggable } from 'react-beautiful-dnd';
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
export default function DNDTagTile(props) {
|
export default function DNDTagTile(props) {
|
||||||
|
|
|
@ -29,6 +29,9 @@ module.exports = React.createClass({
|
||||||
// The primary button which is styled differently and has default focus.
|
// The primary button which is styled differently and has default focus.
|
||||||
primaryButton: PropTypes.node.isRequired,
|
primaryButton: PropTypes.node.isRequired,
|
||||||
|
|
||||||
|
// A node to insert into the cancel button instead of default "Cancel"
|
||||||
|
cancelButton: PropTypes.node,
|
||||||
|
|
||||||
// onClick handler for the primary button.
|
// onClick handler for the primary button.
|
||||||
onPrimaryButtonClick: PropTypes.func.isRequired,
|
onPrimaryButtonClick: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
@ -60,9 +63,9 @@ module.exports = React.createClass({
|
||||||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||||
}
|
}
|
||||||
let cancelButton;
|
let cancelButton;
|
||||||
if (this.props.hasCancel) {
|
if (this.props.cancelButton || this.props.hasCancel) {
|
||||||
cancelButton = <button onClick={this._onCancelClick} disabled={this.props.disabled}>
|
cancelButton = <button onClick={this._onCancelClick} disabled={this.props.disabled}>
|
||||||
{ _t("Cancel") }
|
{ this.props.cancelButton || _t("Cancel") }
|
||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -139,8 +139,11 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
{ editableItems }
|
{ editableItems }
|
||||||
{ this.props.canEdit ?
|
{ this.props.canEdit ?
|
||||||
|
// This is slightly evil; we want a new instance of
|
||||||
|
// EditableItem when the list grows. To make sure it's
|
||||||
|
// reset to its initial state.
|
||||||
<EditableItem
|
<EditableItem
|
||||||
key={-1}
|
key={editableItems.length}
|
||||||
initialValue={this.props.newItem}
|
initialValue={this.props.newItem}
|
||||||
onAdd={this.onItemAdded}
|
onAdd={this.onItemAdded}
|
||||||
onChange={this.onNewItemChanged}
|
onChange={this.onNewItemChanged}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,15 +15,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
import React from 'react';
|
||||||
|
|
||||||
const React = require('react');
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const KEY_TAB = 9;
|
|
||||||
const KEY_SHIFT = 16;
|
|
||||||
const KEY_WINDOWS = 91;
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'EditableText',
|
displayName: 'EditableText',
|
||||||
|
|
||||||
|
@ -66,9 +61,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
if (nextProps.initialValue !== this.props.initialValue ||
|
if (nextProps.initialValue !== this.props.initialValue) {
|
||||||
nextProps.initialValue !== this.value
|
|
||||||
) {
|
|
||||||
this.value = nextProps.initialValue;
|
this.value = nextProps.initialValue;
|
||||||
if (this.refs.editable_div) {
|
if (this.refs.editable_div) {
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
|
@ -139,7 +132,7 @@ module.exports = React.createClass({
|
||||||
this.showPlaceholder(false);
|
this.showPlaceholder(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.key == "Enter") {
|
if (ev.key === "Enter") {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -156,9 +149,9 @@ module.exports = React.createClass({
|
||||||
this.value = ev.target.textContent;
|
this.value = ev.target.textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.key == "Enter") {
|
if (ev.key === "Enter") {
|
||||||
this.onFinish(ev);
|
this.onFinish(ev);
|
||||||
} else if (ev.key == "Escape") {
|
} else if (ev.key === "Escape") {
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +186,7 @@ module.exports = React.createClass({
|
||||||
const submit = (ev.key === "Enter") || shouldSubmit;
|
const submit = (ev.key === "Enter") || shouldSubmit;
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: this.Phases.Display,
|
phase: this.Phases.Display,
|
||||||
}, function() {
|
}, () => {
|
||||||
if (this.value !== this.props.initialValue) {
|
if (this.value !== this.props.initialValue) {
|
||||||
self.onValueChanged(submit);
|
self.onValueChanged(submit);
|
||||||
}
|
}
|
||||||
|
@ -204,23 +197,35 @@ module.exports = React.createClass({
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
|
|
||||||
if (this.props.blurToCancel) {this.cancelEdit();} else {this.onFinish(ev, this.props.blurToSubmit);}
|
if (this.props.blurToCancel) {
|
||||||
|
this.cancelEdit();
|
||||||
|
} else {
|
||||||
|
this.onFinish(ev, this.props.blurToSubmit);
|
||||||
|
}
|
||||||
|
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let editable_el;
|
const {className, editable, initialValue, label, labelClassName} = this.props;
|
||||||
|
let editableEl;
|
||||||
|
|
||||||
if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
|
if (!editable || (this.state.phase === this.Phases.Display && (label || labelClassName) && !this.value)) {
|
||||||
// show the label
|
// show the label
|
||||||
editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{ this.props.label || this.props.initialValue }</div>;
|
editableEl = <div className={className + " " + labelClassName} onClick={this.onClickDiv}>
|
||||||
|
{ label || initialValue }
|
||||||
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||||
editable_el = <div ref="editable_div" contentEditable="true" className={this.props.className}
|
editableEl = <div ref="editable_div"
|
||||||
onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
|
contentEditable={true}
|
||||||
|
className={className}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
onKeyUp={this.onKeyUp}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return editable_el;
|
return editableEl;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,28 +16,24 @@ limitations under the License.
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
// pass in a custom control as the actual body.
|
// pass in a custom control as the actual body.
|
||||||
|
|
||||||
const ContainerId = "mx_PersistedElement";
|
function getOrCreateContainer(containerId) {
|
||||||
|
let container = document.getElementById(containerId);
|
||||||
function getOrCreateContainer() {
|
|
||||||
let container = document.getElementById(ContainerId);
|
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = document.createElement("div");
|
container = document.createElement("div");
|
||||||
container.id = ContainerId;
|
container.id = containerId;
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Greater than that of the ContextualMenu
|
|
||||||
const PE_Z_INDEX = 3000;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Class of component that renders its children in a separate ReactDOM virtual tree
|
* Class of component that renders its children in a separate ReactDOM virtual tree
|
||||||
* in a container element appended to document.body.
|
* in a container element appended to document.body.
|
||||||
|
@ -50,6 +46,14 @@ const PE_Z_INDEX = 3000;
|
||||||
* bounding rect as the parent of PE.
|
* bounding rect as the parent of PE.
|
||||||
*/
|
*/
|
||||||
export default class PersistedElement extends React.Component {
|
export default class PersistedElement extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
// Unique identifier for this PersistedElement instance
|
||||||
|
// Any PersistedElements with the same persistKey will use
|
||||||
|
// the same DOM container.
|
||||||
|
persistKey: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.collectChildContainer = this.collectChildContainer.bind(this);
|
this.collectChildContainer = this.collectChildContainer.bind(this);
|
||||||
|
@ -97,18 +101,16 @@ export default class PersistedElement extends React.Component {
|
||||||
left: parentRect.left + 'px',
|
left: parentRect.left + 'px',
|
||||||
width: parentRect.width + 'px',
|
width: parentRect.width + 'px',
|
||||||
height: parentRect.height + 'px',
|
height: parentRect.height + 'px',
|
||||||
zIndex: PE_Z_INDEX,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content = <div ref={this.collectChild}>
|
const content = <div ref={this.collectChild} style={this.props.style}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
ReactDOM.render(content, getOrCreateContainer());
|
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||||
|
|
||||||
return <div ref={this.collectChildContainer}></div>;
|
return <div ref={this.collectChildContainer}></div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -22,12 +23,13 @@ import PropTypes from 'prop-types';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
||||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||||
|
import FlairStore from "../../../stores/FlairStore";
|
||||||
|
|
||||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||||
|
|
||||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+])[^\/]*)$/;
|
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^\/]*)$/;
|
||||||
|
|
||||||
const Pill = React.createClass({
|
const Pill = React.createClass({
|
||||||
statics: {
|
statics: {
|
||||||
|
@ -45,6 +47,7 @@ const Pill = React.createClass({
|
||||||
},
|
},
|
||||||
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
|
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
|
||||||
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
|
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
|
||||||
|
TYPE_GROUP_MENTION: 'TYPE_GROUP_MENTION',
|
||||||
TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention
|
TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -83,12 +86,14 @@ const Pill = React.createClass({
|
||||||
|
|
||||||
// The member related to the user pill
|
// The member related to the user pill
|
||||||
member: null,
|
member: null,
|
||||||
|
// The group related to the group pill
|
||||||
|
group: null,
|
||||||
// The room related to the room pill
|
// The room related to the room pill
|
||||||
room: null,
|
room: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
async componentWillReceiveProps(nextProps) {
|
||||||
let regex = REGEX_MATRIXTO;
|
let regex = REGEX_MATRIXTO;
|
||||||
if (nextProps.inMessage) {
|
if (nextProps.inMessage) {
|
||||||
regex = REGEX_LOCAL_MATRIXTO;
|
regex = REGEX_LOCAL_MATRIXTO;
|
||||||
|
@ -111,9 +116,11 @@ const Pill = React.createClass({
|
||||||
'@': Pill.TYPE_USER_MENTION,
|
'@': Pill.TYPE_USER_MENTION,
|
||||||
'#': Pill.TYPE_ROOM_MENTION,
|
'#': Pill.TYPE_ROOM_MENTION,
|
||||||
'!': Pill.TYPE_ROOM_MENTION,
|
'!': Pill.TYPE_ROOM_MENTION,
|
||||||
|
'+': Pill.TYPE_GROUP_MENTION,
|
||||||
}[prefix];
|
}[prefix];
|
||||||
|
|
||||||
let member;
|
let member;
|
||||||
|
let group;
|
||||||
let room;
|
let room;
|
||||||
switch (pillType) {
|
switch (pillType) {
|
||||||
case Pill.TYPE_AT_ROOM_MENTION: {
|
case Pill.TYPE_AT_ROOM_MENTION: {
|
||||||
|
@ -142,8 +149,21 @@ const Pill = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Pill.TYPE_GROUP_MENTION: {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
try {
|
||||||
|
group = await FlairStore.getGroupProfileCached(cli, resourceId);
|
||||||
|
} catch (e) { // if FlairStore failed, fall back to just groupId
|
||||||
|
group = {
|
||||||
|
groupId: resourceId,
|
||||||
|
avatarUrl: null,
|
||||||
|
name: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
this.setState({resourceId, pillType, member, room});
|
}
|
||||||
|
}
|
||||||
|
this.setState({resourceId, pillType, member, group, room});
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
|
@ -181,6 +201,7 @@ const Pill = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
||||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||||
|
|
||||||
|
@ -231,6 +252,20 @@ const Pill = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Pill.TYPE_GROUP_MENTION: {
|
||||||
|
if (this.state.group) {
|
||||||
|
const {avatarUrl, groupId, name} = this.state.group;
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
linkText = groupId;
|
||||||
|
if (this.props.shouldShowPillAvatar) {
|
||||||
|
avatar = <BaseAvatar name={name || groupId} width={16} height={16}
|
||||||
|
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
|
||||||
|
}
|
||||||
|
pillClass = 'mx_GroupPill';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = classNames(pillClass, {
|
const classes = classNames(pillClass, {
|
||||||
|
|
|
@ -160,7 +160,7 @@ export default class ReplyThread extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeThread(parentEv, onWidgetLoad, ref) {
|
static makeThread(parentEv, onWidgetLoad, ref) {
|
||||||
if (!SettingsStore.isFeatureEnabled("feature_rich_quoting") || !ReplyThread.getParentEventId(parentEv)) {
|
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} ref={ref} />;
|
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} ref={ref} />;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd.
|
Copyright 2017 New Vector Ltd.
|
||||||
|
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -103,14 +104,27 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onContextButtonClick: function(e) {
|
_openContextMenu: function(x, y, chevronOffset) {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// Hide the (...) immediately
|
// Hide the (...) immediately
|
||||||
this.setState({ hover: false });
|
this.setState({ hover: false });
|
||||||
|
|
||||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||||
|
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||||
|
chevronOffset: chevronOffset,
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
tag: this.props.tag,
|
||||||
|
onFinished: () => {
|
||||||
|
this.setState({ menuDisplayed: false });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.setState({ menuDisplayed: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
onContextButtonClick: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
const elementRect = e.target.getBoundingClientRect();
|
const elementRect = e.target.getBoundingClientRect();
|
||||||
|
|
||||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||||
|
@ -119,17 +133,14 @@ export default React.createClass({
|
||||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||||
|
|
||||||
const self = this;
|
this._openContextMenu(x, y, chevronOffset);
|
||||||
ContextualMenu.createMenu(TagTileContextMenu, {
|
|
||||||
chevronOffset: chevronOffset,
|
|
||||||
left: x,
|
|
||||||
top: y,
|
|
||||||
tag: this.props.tag,
|
|
||||||
onFinished: function() {
|
|
||||||
self.setState({ menuDisplayed: false });
|
|
||||||
},
|
},
|
||||||
});
|
|
||||||
this.setState({ menuDisplayed: true });
|
onContextMenu: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const chevronOffset = 12;
|
||||||
|
this._openContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseOver: function() {
|
onMouseOver: function() {
|
||||||
|
@ -164,7 +175,7 @@ export default React.createClass({
|
||||||
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
|
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
|
||||||
{ "\u00B7\u00B7\u00B7" }
|
{ "\u00B7\u00B7\u00B7" }
|
||||||
</div> : <div />;
|
</div> : <div />;
|
||||||
return <AccessibleButton className={className} onClick={this.onClick}>
|
return <AccessibleButton className={className} onClick={this.onClick} onContextMenu={this.onContextMenu}>
|
||||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
name={name}
|
name={name}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import Analytics from '../../../Analytics';
|
||||||
|
|
||||||
export default class CookieBar extends React.Component {
|
export default class CookieBar extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -29,6 +30,10 @@ export default class CookieBar extends React.Component {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUsageDataClicked() {
|
||||||
|
Analytics.showDetailsModal();
|
||||||
|
}
|
||||||
|
|
||||||
onAccept() {
|
onAccept() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'accept_cookies',
|
action: 'accept_cookies',
|
||||||
|
@ -49,11 +54,18 @@ export default class CookieBar extends React.Component {
|
||||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning" />
|
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning" />
|
||||||
<div className="mx_MatrixToolbar_content">
|
<div className="mx_MatrixToolbar_content">
|
||||||
{ this.props.policyUrl ? _t(
|
{ this.props.policyUrl ? _t(
|
||||||
"Help improve Riot by sending usage data? " +
|
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||||
"This will use a cookie. " +
|
"This will use a cookie " +
|
||||||
"(See our <PolicyLink>cookie and privacy policies</PolicyLink>).",
|
"(please see our <PolicyLink>Cookie Policy</PolicyLink>).",
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
|
'UsageDataLink': (sub) => <a
|
||||||
|
className="mx_MatrixToolbar_link"
|
||||||
|
href="javascript:;"
|
||||||
|
onClick={this.onUsageDataClicked}
|
||||||
|
>
|
||||||
|
{ sub }
|
||||||
|
</a>,
|
||||||
// XXX: We need to link to the page that explains our cookies
|
// XXX: We need to link to the page that explains our cookies
|
||||||
'PolicyLink': (sub) => <a
|
'PolicyLink': (sub) => <a
|
||||||
className="mx_MatrixToolbar_link"
|
className="mx_MatrixToolbar_link"
|
||||||
|
@ -64,10 +76,23 @@ export default class CookieBar extends React.Component {
|
||||||
</a>
|
</a>
|
||||||
,
|
,
|
||||||
},
|
},
|
||||||
) : _t("Help improve Riot by sending usage data? This will use a cookie.") }
|
) : _t(
|
||||||
|
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||||
|
"This will use a cookie.",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'UsageDataLink': (sub) => <a
|
||||||
|
className="mx_MatrixToolbar_link"
|
||||||
|
href="javascript:;"
|
||||||
|
onClick={this.onUsageDataClicked}
|
||||||
|
>
|
||||||
|
{ sub }
|
||||||
|
</a>,
|
||||||
|
},
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton element='button' className="mx_MatrixToolbar_action" onClick={this.onAccept}>
|
<AccessibleButton element='button' className="mx_MatrixToolbar_action" onClick={this.onAccept}>
|
||||||
{ _t("Yes please") }
|
{ _t("Yes, I want to help!") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
|
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
|
||||||
<img src="img/cancel.svg" width="18" height="18" />
|
<img src="img/cancel.svg" width="18" height="18" />
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,28 +15,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import dis from '../../../dispatcher';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
onUpdateClicked: function() {
|
onUpdateClicked: function() {
|
||||||
const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog');
|
const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog');
|
||||||
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog, {
|
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
|
||||||
onFinished: (passwordChanged) => {
|
|
||||||
if (!passwordChanged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Notify SessionStore that the user's password was changed
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'password_changed',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -20,6 +21,9 @@ import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
import {createMenu} from "../../structures/ContextualMenu";
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'GroupInviteTile',
|
displayName: 'GroupInviteTile',
|
||||||
|
@ -32,6 +36,15 @@ export default React.createClass({
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return ({
|
||||||
|
hover: false,
|
||||||
|
badgeHover: false,
|
||||||
|
menuDisplayed: false,
|
||||||
|
selected: this.props.group.groupId === null, // XXX: this needs linking to LoggedInView/GroupView state
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onClick: function(e) {
|
onClick: function(e) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_group',
|
action: 'view_group',
|
||||||
|
@ -39,6 +52,69 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onMouseEnter: function() {
|
||||||
|
const state = {hover: true};
|
||||||
|
// Only allow non-guests to access the context menu
|
||||||
|
if (!this.context.matrixClient.isGuest()) {
|
||||||
|
state.badgeHover = true;
|
||||||
|
}
|
||||||
|
this.setState(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseLeave: function() {
|
||||||
|
this.setState({
|
||||||
|
badgeHover: false,
|
||||||
|
hover: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_showContextMenu: function(x, y, chevronOffset) {
|
||||||
|
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
|
||||||
|
|
||||||
|
createMenu(GroupInviteTileContextMenu, {
|
||||||
|
chevronOffset,
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
group: this.props.group,
|
||||||
|
onFinished: () => {
|
||||||
|
this.setState({ menuDisplayed: false });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.setState({ menuDisplayed: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
onContextMenu: function(e) {
|
||||||
|
// Prevent the RoomTile onClick event firing as well
|
||||||
|
e.preventDefault();
|
||||||
|
// Only allow non-guests to access the context menu
|
||||||
|
if (MatrixClientPeg.get().isGuest()) return;
|
||||||
|
|
||||||
|
const chevronOffset = 12;
|
||||||
|
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||||
|
},
|
||||||
|
|
||||||
|
onBadgeClicked: function(e) {
|
||||||
|
// Prevent the RoomTile onClick event firing as well
|
||||||
|
e.stopPropagation();
|
||||||
|
// Only allow non-guests to access the context menu
|
||||||
|
if (MatrixClientPeg.get().isGuest()) return;
|
||||||
|
|
||||||
|
// If the badge is clicked, then no longer show tooltip
|
||||||
|
if (this.props.collapsed) {
|
||||||
|
this.setState({ hover: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const elementRect = e.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||||
|
const x = elementRect.right + window.pageXOffset + 3;
|
||||||
|
const chevronOffset = 12;
|
||||||
|
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||||
|
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||||
|
|
||||||
|
this._showContextMenu(x, y, chevronOffset);
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
|
@ -49,19 +125,40 @@ export default React.createClass({
|
||||||
|
|
||||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
||||||
|
|
||||||
const label = <EmojiText
|
const nameClasses = classNames('mx_RoomTile_name mx_RoomTile_invite mx_RoomTile_badgeShown', {
|
||||||
element="div"
|
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
|
||||||
title={this.props.group.groupId}
|
});
|
||||||
className="mx_RoomTile_name mx_RoomTile_badgeShown"
|
|
||||||
dir="auto"
|
const label = <EmojiText element="div" title={this.props.group.groupId} className={nameClasses} dir="auto">
|
||||||
>
|
|
||||||
{ groupName }
|
{ groupName }
|
||||||
</EmojiText>;
|
</EmojiText>;
|
||||||
|
|
||||||
const badge = <div className="mx_RoomSubList_badge mx_RoomSubList_badgeHighlight">!</div>;
|
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
|
||||||
|
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
|
||||||
|
'mx_RoomTile_badgeButton': badgeEllipsis,
|
||||||
|
});
|
||||||
|
|
||||||
|
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
|
||||||
|
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||||
|
|
||||||
|
let tooltip;
|
||||||
|
if (this.props.collapsed && this.state.hover) {
|
||||||
|
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||||
|
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={groupName} dir="auto" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
|
||||||
|
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||||
|
'mx_RoomTile_selected': this.state.selected,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className="mx_RoomTile mx_RoomTile_highlight" onClick={this.onClick}>
|
<AccessibleButton className={classes}
|
||||||
|
onClick={this.onClick}
|
||||||
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
onContextMenu={this.onContextMenu}
|
||||||
|
>
|
||||||
<div className="mx_RoomTile_avatar">
|
<div className="mx_RoomTile_avatar">
|
||||||
{ av }
|
{ av }
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,6 +166,7 @@ export default React.createClass({
|
||||||
{ label }
|
{ label }
|
||||||
{ badge }
|
{ badge }
|
||||||
</div>
|
</div>
|
||||||
|
{ tooltip }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -69,7 +69,7 @@ export default React.createClass({
|
||||||
render() {
|
render() {
|
||||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||||
const input = <input type="checkbox"
|
const input = <input type="checkbox"
|
||||||
onClick={this._onPublicityToggle}
|
onChange={this._onPublicityToggle}
|
||||||
checked={this.state.isGroupPublicised}
|
checked={this.state.isGroupPublicised}
|
||||||
/>;
|
/>;
|
||||||
const labelText = !this.state.ready ? _t("Loading...") :
|
const labelText = !this.state.ready ? _t("Loading...") :
|
||||||
|
|
|
@ -22,6 +22,7 @@ import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
|
|
||||||
|
function nop() {}
|
||||||
|
|
||||||
const GroupTile = React.createClass({
|
const GroupTile = React.createClass({
|
||||||
displayName: 'GroupTile',
|
displayName: 'GroupTile',
|
||||||
|
@ -81,7 +82,7 @@ const GroupTile = React.createClass({
|
||||||
) : null;
|
) : null;
|
||||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
|
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
|
||||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown}>
|
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}>
|
||||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||||
{ (droppableProvided, droppableSnapshot) => (
|
{ (droppableProvided, droppableSnapshot) => (
|
||||||
<div ref={droppableProvided.innerRef}>
|
<div ref={droppableProvided.innerRef}>
|
||||||
|
|
|
@ -28,6 +28,7 @@ import SdkConfig from '../../../SdkConfig';
|
||||||
*/
|
*/
|
||||||
class PasswordLogin extends React.Component {
|
class PasswordLogin extends React.Component {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
onError: function() {},
|
||||||
onUsernameChanged: function() {},
|
onUsernameChanged: function() {},
|
||||||
onPasswordChanged: function() {},
|
onPasswordChanged: function() {},
|
||||||
onPhoneCountryChanged: function() {},
|
onPhoneCountryChanged: function() {},
|
||||||
|
@ -56,33 +57,64 @@ class PasswordLogin extends React.Component {
|
||||||
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
||||||
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
||||||
this.onPasswordChanged = this.onPasswordChanged.bind(this);
|
this.onPasswordChanged = this.onPasswordChanged.bind(this);
|
||||||
|
this.isLoginEmpty = this.isLoginEmpty.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this._passwordField = null;
|
this._passwordField = null;
|
||||||
|
this._loginField = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
|
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
|
||||||
field_input_incorrect(this._passwordField);
|
field_input_incorrect(this.isLoginEmpty() ? this._loginField : this._passwordField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmitForm(ev) {
|
onSubmitForm(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
|
|
||||||
this.props.onSubmit(
|
let username = ''; // XXX: Synapse breaks if you send null here:
|
||||||
'', // XXX: Synapse breaks if you send null here:
|
let phoneCountry = null;
|
||||||
this.state.phoneCountry,
|
let phoneNumber = null;
|
||||||
this.state.phoneNumber,
|
let error;
|
||||||
this.state.password,
|
|
||||||
);
|
switch (this.state.loginType) {
|
||||||
|
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||||
|
username = this.state.username;
|
||||||
|
if (!username) {
|
||||||
|
error = _t('The email field must not be blank.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||||
|
username = this.state.username;
|
||||||
|
if (!username) {
|
||||||
|
error = _t('The user name field must not be blank.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||||
|
phoneCountry = this.state.phoneCountry;
|
||||||
|
phoneNumber = this.state.phoneNumber;
|
||||||
|
if (!phoneNumber) {
|
||||||
|
error = _t('The phone number field must not be blank.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
this.props.onError(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.state.password) {
|
||||||
|
this.props.onError(_t('The password field must not be blank.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.props.onSubmit(
|
this.props.onSubmit(
|
||||||
this.state.username,
|
username,
|
||||||
null,
|
phoneCountry,
|
||||||
null,
|
phoneNumber,
|
||||||
this.state.password,
|
this.state.password,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -93,6 +125,7 @@ class PasswordLogin extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoginTypeChange(loginType) {
|
onLoginTypeChange(loginType) {
|
||||||
|
this.props.onError(null); // send a null error to clear any error messages
|
||||||
this.setState({
|
this.setState({
|
||||||
loginType: loginType,
|
loginType: loginType,
|
||||||
username: "", // Reset because email and username use the same state
|
username: "", // Reset because email and username use the same state
|
||||||
|
@ -126,8 +159,10 @@ class PasswordLogin extends React.Component {
|
||||||
switch (loginType) {
|
switch (loginType) {
|
||||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||||
classes.mx_Login_email = true;
|
classes.mx_Login_email = true;
|
||||||
|
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||||
return <input
|
return <input
|
||||||
className={classNames(classes)}
|
className={classNames(classes)}
|
||||||
|
ref={(e) => {this._loginField = e;}}
|
||||||
key="email_input"
|
key="email_input"
|
||||||
type="text"
|
type="text"
|
||||||
name="username" // make it a little easier for browser's remember-password
|
name="username" // make it a little easier for browser's remember-password
|
||||||
|
@ -139,8 +174,10 @@ class PasswordLogin extends React.Component {
|
||||||
/>;
|
/>;
|
||||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||||
classes.mx_Login_username = true;
|
classes.mx_Login_username = true;
|
||||||
|
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||||
return <input
|
return <input
|
||||||
className={classNames(classes)}
|
className={classNames(classes)}
|
||||||
|
ref={(e) => {this._loginField = e;}}
|
||||||
key="username_input"
|
key="username_input"
|
||||||
type="text"
|
type="text"
|
||||||
name="username" // make it a little easier for browser's remember-password
|
name="username" // make it a little easier for browser's remember-password
|
||||||
|
@ -153,14 +190,14 @@ class PasswordLogin extends React.Component {
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>;
|
/>;
|
||||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
case PasswordLogin.LOGIN_FIELD_PHONE: {
|
||||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||||
classes.mx_Login_phoneNumberField = true;
|
classes.mx_Login_phoneNumberField = true;
|
||||||
classes.mx_Login_field_has_prefix = true;
|
classes.mx_Login_field_has_prefix = true;
|
||||||
|
classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
|
||||||
return <div className="mx_Login_phoneSection">
|
return <div className="mx_Login_phoneSection">
|
||||||
<CountryDropdown
|
<CountryDropdown
|
||||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||||
ref="phone_country"
|
|
||||||
onOptionChange={this.onPhoneCountryChanged}
|
onOptionChange={this.onPhoneCountryChanged}
|
||||||
value={this.state.phoneCountry}
|
value={this.state.phoneCountry}
|
||||||
isSmall={true}
|
isSmall={true}
|
||||||
|
@ -169,7 +206,7 @@ class PasswordLogin extends React.Component {
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className={classNames(classes)}
|
className={classNames(classes)}
|
||||||
ref="phoneNumber"
|
ref={(e) => {this._loginField = e;}}
|
||||||
key="phone_input"
|
key="phone_input"
|
||||||
type="text"
|
type="text"
|
||||||
name="phoneNumber"
|
name="phoneNumber"
|
||||||
|
@ -182,6 +219,17 @@ class PasswordLogin extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoginEmpty() {
|
||||||
|
switch (this.state.loginType) {
|
||||||
|
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||||
|
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||||
|
return !this.state.username;
|
||||||
|
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||||
|
return !this.state.phoneCountry || !this.state.phoneNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let forgotPasswordJsx;
|
let forgotPasswordJsx;
|
||||||
|
@ -207,7 +255,7 @@ class PasswordLogin extends React.Component {
|
||||||
const pwFieldClass = classNames({
|
const pwFieldClass = classNames({
|
||||||
mx_Login_field: true,
|
mx_Login_field: true,
|
||||||
mx_Login_field_disabled: matrixIdText === '',
|
mx_Login_field_disabled: matrixIdText === '',
|
||||||
error: this.props.loginIncorrect,
|
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||||
});
|
});
|
||||||
|
|
||||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||||
|
@ -258,6 +306,7 @@ PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
|
||||||
|
|
||||||
PasswordLogin.propTypes = {
|
PasswordLogin.propTypes = {
|
||||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
||||||
|
onError: PropTypes.func,
|
||||||
onForgotPasswordClick: PropTypes.func, // fn()
|
onForgotPasswordClick: PropTypes.func, // fn()
|
||||||
initialUsername: PropTypes.string,
|
initialUsername: PropTypes.string,
|
||||||
initialPhoneCountry: PropTypes.string,
|
initialPhoneCountry: PropTypes.string,
|
||||||
|
|
|
@ -327,6 +327,7 @@ module.exports = React.createClass({
|
||||||
// will have the correct name when the user tries to download it.
|
// will have the correct name when the user tries to download it.
|
||||||
// We can't provide a Content-Disposition header like we would for HTTP.
|
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||||
download: fileName,
|
download: fileName,
|
||||||
|
rel: "noopener",
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
textContent: _t("Download %(text)s", { text: text }),
|
textContent: _t("Download %(text)s", { text: text }),
|
||||||
}, "*");
|
}, "*");
|
||||||
|
|