Merge branch 'develop' into t3chguy/warn_self_mute
|
@ -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
|
||||||
|
|
114
CHANGELOG.md
|
@ -1,3 +1,117 @@
|
||||||
|
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)
|
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)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.7-rc.1...v0.12.7)
|
||||||
|
|
2208
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.12.7",
|
"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": {
|
||||||
|
@ -73,13 +73,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.4",
|
"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",
|
||||||
|
|
|
@ -42,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%;
|
||||||
}
|
}
|
||||||
|
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,14 +37,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: absolute;
|
||||||
position: relative;
|
top: 50%;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MStickerBody_image {
|
|
||||||
max-width: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MStickerBody_image_visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MStickerBody_placeholder {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
||||||
|
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,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() {
|
||||||
|
|
|
@ -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,66 +248,58 @@ function _onAction(payload) {
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'place_call':
|
case 'place_call':
|
||||||
if (module.exports.getAnyActiveCall()) {
|
{
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
if (module.exports.getAnyActiveCall()) {
|
||||||
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
title: _t('Existing Call'),
|
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||||
description: _t('You are already in a call.'),
|
title: _t('Existing Call'),
|
||||||
});
|
description: _t('You are already in a call.'),
|
||||||
return; // don't allow >1 call to be placed.
|
});
|
||||||
}
|
return; // don't allow >1 call to be placed.
|
||||||
|
}
|
||||||
|
|
||||||
// if the runtime env doesn't do VoIP, whine.
|
// if the runtime env doesn't do VoIP, whine.
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||||
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.'),
|
||||||
});
|
});
|
||||||
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, {
|
||||||
description: _t('You cannot place a call with yourself.'),
|
description: _t('You cannot place a call with yourself.'),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} else if (members.length === 2) {
|
} else if (members.length === 2) {
|
||||||
console.log("Place %s call in %s", payload.type, payload.room_id);
|
console.log("Place %s call in %s", payload.type, payload.room_id);
|
||||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
|
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
|
||||||
placeCall(call);
|
placeCall(call);
|
||||||
} else { // > 2
|
} else { // > 2
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "place_conference_call",
|
action: "place_conference_call",
|
||||||
room_id: payload.room_id,
|
room_id: payload.room_id,
|
||||||
type: payload.type,
|
type: payload.type,
|
||||||
remote_element: payload.remote_element,
|
remote_element: payload.remote_element,
|
||||||
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 (!ConferenceHandler) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
|
||||||
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
|
||||||
description: _t('Conference calls are not supported in this client'),
|
|
||||||
});
|
|
||||||
} else if (!MatrixClientPeg.get().supportsVoip()) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
|
||||||
title: _t('VoIP is unsupported'),
|
|
||||||
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
|
// Conference calls are implemented by sending the media to central
|
||||||
// server which combines the audio from all the participants together
|
// server which combines the audio from all the participants together
|
||||||
// into a single stream. This is incompatible with end-to-end encryption
|
// into a single stream. This is incompatible with end-to-end encryption
|
||||||
|
@ -316,47 +310,75 @@ function _onAction(payload) {
|
||||||
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
|
||||||
description: _t('Conference calls are not supported in encrypted rooms'),
|
description: _t('Conference calls are not supported in encrypted rooms'),
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsStore.isFeatureEnabled('feature_jitsi')) {
|
||||||
|
_startCallApp(payload.room_id, payload.type);
|
||||||
} else {
|
} else {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
if (!ConferenceHandler) {
|
||||||
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
title: _t('Warning!'),
|
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
||||||
description: _t('Conference calling is in development and may not be reliable.'),
|
description: _t('Conference calls are not supported in this client'),
|
||||||
onFinished: (confirm)=>{
|
});
|
||||||
if (confirm) {
|
} else if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
ConferenceHandler.createNewMatrixCall(
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
MatrixClientPeg.get(), payload.room_id,
|
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||||
).done(function(call) {
|
title: _t('VoIP is unsupported'),
|
||||||
placeCall(call);
|
description: _t('You cannot place VoIP calls in this browser.'),
|
||||||
}, function(err) {
|
});
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
} else {
|
||||||
console.error("Conference call failed: " + err);
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createTrackedDialog('Call Handler', 'Failed to set up conference call', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
||||||
title: _t('Failed to set up conference call'),
|
title: _t('Warning!'),
|
||||||
description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
|
description: _t('Conference calling is in development and may not be reliable.'),
|
||||||
|
onFinished: (confirm)=>{
|
||||||
|
if (confirm) {
|
||||||
|
ConferenceHandler.createNewMatrixCall(
|
||||||
|
MatrixClientPeg.get(), payload.room_id,
|
||||||
|
).done(function(call) {
|
||||||
|
placeCall(call);
|
||||||
|
}, function(err) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Conference call failed: " + err);
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Call Handler',
|
||||||
|
'Failed to set up conference call',
|
||||||
|
ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t('Failed to set up conference call'),
|
||||||
|
description: (
|
||||||
|
_t('Conference call failed.') +
|
||||||
|
' ' + ((err && err.message) ? err.message : '')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
},
|
||||||
},
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'incoming_call':
|
case 'incoming_call':
|
||||||
if (module.exports.getAnyActiveCall()) {
|
{
|
||||||
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
|
if (module.exports.getAnyActiveCall()) {
|
||||||
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
|
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
|
||||||
// in future we could signal a "local busy" as a warning to the caller.
|
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
|
||||||
// see https://github.com/vector-im/vector-web/issues/1964
|
// in future we could signal a "local busy" as a warning to the caller.
|
||||||
return;
|
// see https://github.com/vector-im/vector-web/issues/1964
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// if the runtime env doesn't do VoIP, stop here.
|
// if the runtime env doesn't do VoIP, stop here.
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
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
|
||||||
|
|
172
src/DecryptionFailureTracker.js
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DecryptionFailure {
|
||||||
|
constructor(failedEventId) {
|
||||||
|
this.failedEventId = failedEventId;
|
||||||
|
this.ts = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default 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 added to `failuresToTrack`.
|
||||||
|
failures = [];
|
||||||
|
|
||||||
|
// Every TRACK_INTERVAL_MS (so as to spread the number of hits done on Analytics),
|
||||||
|
// one DecryptionFailure of this FIFO is removed and tracked.
|
||||||
|
failuresToTrack = [];
|
||||||
|
|
||||||
|
// 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 moving
|
||||||
|
// the failure to `failuresToTrack`.
|
||||||
|
static GRACE_PERIOD_MS = 60000;
|
||||||
|
|
||||||
|
constructor(fn) {
|
||||||
|
if (!fn || typeof fn !== 'function') {
|
||||||
|
throw new Error('DecryptionFailureTracker requires tracking function');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.trackDecryptionFailure = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
if (e.isDecryptionFailure()) {
|
||||||
|
this.addDecryptionFailureForEvent(e);
|
||||||
|
} else {
|
||||||
|
// Could be an event in the failures, remove it
|
||||||
|
this.removeDecryptionFailuresForEvent(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addDecryptionFailureForEvent(e) {
|
||||||
|
this.failures.push(new DecryptionFailure(e.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.trackFailure(),
|
||||||
|
DecryptionFailureTracker.TRACK_INTERVAL_MS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear state and stop checking for and tracking failures.
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
clearInterval(this.checkInterval);
|
||||||
|
clearInterval(this.trackInterval);
|
||||||
|
|
||||||
|
this.failures = [];
|
||||||
|
this.failuresToTrack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.failuresToTrack = [...this.failuresToTrack, ...dedupedFailures];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there are failures that should be tracked, call the given trackDecryptionFailure
|
||||||
|
* function with the number of failures that should be tracked.
|
||||||
|
*/
|
||||||
|
trackFailure() {
|
||||||
|
if (this.failuresToTrack.length > 0) {
|
||||||
|
// Remove all failures, and expose the number of failures for now.
|
||||||
|
//
|
||||||
|
// TODO: Track a histogram of error types to cardinailty to allow for
|
||||||
|
// aggregation by error type.
|
||||||
|
this.trackDecryptionFailure(this.failuresToTrack.splice(0).length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -216,10 +216,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]) {
|
||||||
attribs.href = '#/user/' + entity;
|
case '@':
|
||||||
} else if (entity[0] === '#' || entity[0] === '!') {
|
attribs.href = '#/user/' + entity;
|
||||||
attribs.href = '#/room/' + entity;
|
break;
|
||||||
|
case '+':
|
||||||
|
attribs.href = '#/group/' + entity;
|
||||||
|
break;
|
||||||
|
case '#':
|
||||||
|
case '!':
|
||||||
|
attribs.href = '#/room/' + entity;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
delete attribs.target;
|
delete attribs.target;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,80 +588,63 @@ const onMessage = function(event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let promise = Promise.resolve(currentRoomId);
|
|
||||||
if (!currentRoomId) {
|
if (roomId !== RoomViewStore.getRoomId()) {
|
||||||
if (!currentRoomAlias) {
|
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
|
||||||
sendError(event, _t('Must be viewing a room'));
|
return;
|
||||||
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) => {
|
// Get and set room-based widgets
|
||||||
if (roomId !== viewingRoomId) {
|
if (event.data.action === "get_widgets") {
|
||||||
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
|
getWidgets(event, roomId);
|
||||||
return;
|
return;
|
||||||
}
|
} else if (event.data.action === "set_widget") {
|
||||||
|
setWidget(event, roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get and set room-based widgets
|
// These APIs don't require userId
|
||||||
if (event.data.action === "get_widgets") {
|
if (event.data.action === "join_rules_state") {
|
||||||
getWidgets(event, roomId);
|
getJoinRules(event, roomId);
|
||||||
return;
|
return;
|
||||||
} else if (event.data.action === "set_widget") {
|
} else if (event.data.action === "set_plumbing_state") {
|
||||||
setWidget(event, roomId);
|
setPlumbingState(event, roomId, event.data.status);
|
||||||
return;
|
return;
|
||||||
}
|
} else if (event.data.action === "get_membership_count") {
|
||||||
|
getMembershipCount(event, roomId);
|
||||||
|
return;
|
||||||
|
} else if (event.data.action === "get_room_enc_state") {
|
||||||
|
getRoomEncState(event, roomId);
|
||||||
|
return;
|
||||||
|
} else if (event.data.action === "can_send_event") {
|
||||||
|
canSendEvent(event, roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// These APIs don't require userId
|
if (!userId) {
|
||||||
if (event.data.action === "join_rules_state") {
|
sendError(event, _t('Missing user_id in request'));
|
||||||
getJoinRules(event, roomId);
|
return;
|
||||||
return;
|
}
|
||||||
} else if (event.data.action === "set_plumbing_state") {
|
switch (event.data.action) {
|
||||||
setPlumbingState(event, roomId, event.data.status);
|
case "membership_state":
|
||||||
return;
|
getMembershipState(event, roomId, userId);
|
||||||
} else if (event.data.action === "get_membership_count") {
|
break;
|
||||||
getMembershipCount(event, roomId);
|
case "invite":
|
||||||
return;
|
inviteUser(event, roomId, userId);
|
||||||
} else if (event.data.action === "get_room_enc_state") {
|
break;
|
||||||
getRoomEncState(event, roomId);
|
case "bot_options":
|
||||||
return;
|
botOptions(event, roomId, userId);
|
||||||
} else if (event.data.action === "can_send_event") {
|
break;
|
||||||
canSendEvent(event, roomId);
|
case "set_bot_options":
|
||||||
return;
|
setBotOptions(event, roomId, userId);
|
||||||
}
|
break;
|
||||||
|
case "set_bot_power":
|
||||||
if (!userId) {
|
setBotPower(event, roomId, userId, event.data.level);
|
||||||
sendError(event, _t('Missing user_id in request'));
|
break;
|
||||||
return;
|
default:
|
||||||
}
|
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
||||||
switch (event.data.action) {
|
break;
|
||||||
case "membership_state":
|
}
|
||||||
getMembershipState(event, roomId, userId);
|
|
||||||
break;
|
|
||||||
case "invite":
|
|
||||||
inviteUser(event, roomId, userId);
|
|
||||||
break;
|
|
||||||
case "bot_options":
|
|
||||||
botOptions(event, roomId, userId);
|
|
||||||
break;
|
|
||||||
case "set_bot_options":
|
|
||||||
setBotOptions(event, roomId, userId);
|
|
||||||
break;
|
|
||||||
case "set_bot_power":
|
|
||||||
setBotPower(event, roomId, userId, event.data.level);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
|
||||||
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,352 +64,408 @@ 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({
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
name: 'ddg',
|
||||||
// TODO Don't explain this away, actually show a search UI here.
|
args: '<query>',
|
||||||
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
|
description: _td('Searches DuckDuckGo for results'),
|
||||||
title: _t('/ddg is not a command'),
|
runFn: function(roomId, args) {
|
||||||
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
});
|
// TODO Don't explain this away, actually show a search UI here.
|
||||||
return success();
|
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
|
||||||
|
title: _t('/ddg is not a command'),
|
||||||
|
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
||||||
|
});
|
||||||
|
return success();
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Change your nickname
|
nick: new Command({
|
||||||
nick: new Command("nick", "<display_name>", function(roomId, args) {
|
name: 'nick',
|
||||||
if (args) {
|
args: '<display_name>',
|
||||||
return success(
|
description: _td('Changes your display nickname'),
|
||||||
MatrixClientPeg.get().setDisplayName(args),
|
runFn: function(roomId, args) {
|
||||||
);
|
if (args) {
|
||||||
}
|
return success(MatrixClientPeg.get().setDisplayName(args));
|
||||||
return reject(this.getUsage());
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Changes the colorscheme of your current room
|
|
||||||
tint: new Command("tint", "<color1> [<color2>]", function(roomId, 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})))?$/);
|
|
||||||
if (matches) {
|
|
||||||
Tinter.tint(matches[1], matches[4]);
|
|
||||||
const colorScheme = {};
|
|
||||||
colorScheme.primary_color = matches[1];
|
|
||||||
if (matches[4]) {
|
|
||||||
colorScheme.secondary_color = matches[4];
|
|
||||||
} else {
|
|
||||||
colorScheme.secondary_color = colorScheme.primary_color;
|
|
||||||
}
|
|
||||||
return success(
|
|
||||||
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Change the room topic
|
tint: new Command({
|
||||||
topic: new Command("topic", "<topic>", function(roomId, args) {
|
name: 'tint',
|
||||||
if (args) {
|
args: '<color1> [<color2>]',
|
||||||
return success(
|
description: _td('Changes colour scheme of current room'),
|
||||||
MatrixClientPeg.get().setRoomTopic(roomId, args),
|
runFn: function(roomId, args) {
|
||||||
);
|
if (args) {
|
||||||
}
|
const matches = args.match(/^(#([\da-fA-F]{3}|[\da-fA-F]{6}))( +(#([\da-fA-F]{3}|[\da-fA-F]{6})))?$/);
|
||||||
return reject(this.getUsage());
|
if (matches) {
|
||||||
}),
|
Tinter.tint(matches[1], matches[4]);
|
||||||
|
const colorScheme = {};
|
||||||
// Invite a user
|
colorScheme.primary_color = matches[1];
|
||||||
invite: new Command("invite", "<userId>", function(roomId, args) {
|
if (matches[4]) {
|
||||||
if (args) {
|
colorScheme.secondary_color = matches[4];
|
||||||
const matches = args.match(/^(\S+)$/);
|
} else {
|
||||||
if (matches) {
|
colorScheme.secondary_color = colorScheme.primary_color;
|
||||||
return success(
|
|
||||||
MatrixClientPeg.get().invite(roomId, matches[1]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reject(this.getUsage());
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Join a room
|
|
||||||
join: new Command("join", "#alias:domain", function(roomId, args) {
|
|
||||||
if (args) {
|
|
||||||
const matches = args.match(/^(\S+)$/);
|
|
||||||
if (matches) {
|
|
||||||
let roomAlias = matches[1];
|
|
||||||
if (roomAlias[0] !== '#') {
|
|
||||||
return reject(this.getUsage());
|
|
||||||
}
|
|
||||||
if (!roomAlias.match(/:/)) {
|
|
||||||
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
|
||||||
}
|
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_alias: roomAlias,
|
|
||||||
auto_join: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return success();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reject(this.getUsage());
|
|
||||||
}),
|
|
||||||
|
|
||||||
part: new Command("part", "[#alias:domain]", function(roomId, args) {
|
|
||||||
let targetRoomId;
|
|
||||||
if (args) {
|
|
||||||
const matches = args.match(/^(\S+)$/);
|
|
||||||
if (matches) {
|
|
||||||
let roomAlias = matches[1];
|
|
||||||
if (roomAlias[0] !== '#') {
|
|
||||||
return reject(this.getUsage());
|
|
||||||
}
|
|
||||||
if (!roomAlias.match(/:/)) {
|
|
||||||
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find a room with this alias
|
|
||||||
const rooms = MatrixClientPeg.get().getRooms();
|
|
||||||
for (let i = 0; i < rooms.length; i++) {
|
|
||||||
const aliasEvents = rooms[i].currentState.getStateEvents(
|
|
||||||
"m.room.aliases",
|
|
||||||
);
|
|
||||||
for (let j = 0; j < aliasEvents.length; j++) {
|
|
||||||
const aliases = aliasEvents[j].getContent().aliases || [];
|
|
||||||
for (let k = 0; k < aliases.length; k++) {
|
|
||||||
if (aliases[k] === roomAlias) {
|
|
||||||
targetRoomId = rooms[i].roomId;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (targetRoomId) { break; }
|
|
||||||
}
|
}
|
||||||
if (targetRoomId) { break; }
|
return success(
|
||||||
}
|
SettingsStore.setValue('roomColor', roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
||||||
if (!targetRoomId) {
|
);
|
||||||
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
if (!targetRoomId) targetRoomId = roomId;
|
},
|
||||||
return success(
|
|
||||||
MatrixClientPeg.get().leave(targetRoomId).then(
|
|
||||||
function() {
|
|
||||||
dis.dispatch({action: 'view_next_room'});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Kick a user from the room with an optional reason
|
topic: new Command({
|
||||||
kick: new Command("kick", "<userId> [<reason>]", function(roomId, args) {
|
name: 'topic',
|
||||||
if (args) {
|
args: '<topic>',
|
||||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
description: _td('Sets the room topic'),
|
||||||
if (matches) {
|
runFn: function(roomId, args) {
|
||||||
return success(
|
if (args) {
|
||||||
MatrixClientPeg.get().kick(roomId, matches[1], matches[3]),
|
return success(MatrixClientPeg.get().setRoomTopic(roomId, args));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
invite: new Command({
|
||||||
|
name: 'invite',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Invites user with given id to current room'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
return success(MatrixClientPeg.get().invite(roomId, matches[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
join: new Command({
|
||||||
|
name: 'join',
|
||||||
|
args: '<room-alias>',
|
||||||
|
description: _td('Joins room with given alias'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
let roomAlias = matches[1];
|
||||||
|
if (roomAlias[0] !== '#') return reject(this.getUsage());
|
||||||
|
|
||||||
|
if (!roomAlias.includes(':')) {
|
||||||
|
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
||||||
|
}
|
||||||
|
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_alias: roomAlias,
|
||||||
|
auto_join: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
part: new Command({
|
||||||
|
name: 'part',
|
||||||
|
args: '[<room-alias>]',
|
||||||
|
description: _td('Leave room'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
let targetRoomId;
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
let roomAlias = matches[1];
|
||||||
|
if (roomAlias[0] !== '#') return reject(this.getUsage());
|
||||||
|
|
||||||
|
if (!roomAlias.includes(':')) {
|
||||||
|
roomAlias += ':' + cli.getDomain();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find a room with this alias
|
||||||
|
const rooms = cli.getRooms();
|
||||||
|
for (let i = 0; i < rooms.length; i++) {
|
||||||
|
const aliasEvents = rooms[i].currentState.getStateEvents('m.room.aliases');
|
||||||
|
for (let j = 0; j < aliasEvents.length; j++) {
|
||||||
|
const aliases = aliasEvents[j].getContent().aliases || [];
|
||||||
|
for (let k = 0; k < aliases.length; k++) {
|
||||||
|
if (aliases[k] === roomAlias) {
|
||||||
|
targetRoomId = rooms[i].roomId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetRoomId) break;
|
||||||
|
}
|
||||||
|
if (targetRoomId) break;
|
||||||
|
}
|
||||||
|
if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetRoomId) targetRoomId = roomId;
|
||||||
|
return success(
|
||||||
|
cli.leave(targetRoomId).then(function() {
|
||||||
|
dis.dispatch({action: 'view_next_room'});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
kick: new Command({
|
||||||
|
name: 'kick',
|
||||||
|
args: '<user-id> [reason]',
|
||||||
|
description: _td('Kicks user with given id'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
|
if (matches) {
|
||||||
|
return success(MatrixClientPeg.get().kick(roomId, matches[1], matches[3]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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({
|
||||||
if (args) {
|
name: 'ban',
|
||||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
args: '<user-id> [reason]',
|
||||||
if (matches) {
|
description: _td('Bans user with given id'),
|
||||||
return success(
|
runFn: function(roomId, args) {
|
||||||
MatrixClientPeg.get().ban(roomId, matches[1], matches[3]),
|
if (args) {
|
||||||
);
|
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
|
if (matches) {
|
||||||
|
return success(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({
|
||||||
if (args) {
|
name: 'unban',
|
||||||
const matches = args.match(/^(\S+)$/);
|
args: '<user-id>',
|
||||||
if (matches) {
|
description: _td('Unbans user with given id'),
|
||||||
// Reset the user membership to "leave" to unban him
|
runFn: function(roomId, args) {
|
||||||
return success(
|
if (args) {
|
||||||
MatrixClientPeg.get().unban(roomId, matches[1]),
|
const matches = args.match(/^(\S+)$/);
|
||||||
);
|
if (matches) {
|
||||||
|
// Reset the user membership to "leave" to unban him
|
||||||
|
return success(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({
|
||||||
if (args) {
|
name: 'ignore',
|
||||||
const matches = args.match(/^(\S+)$/);
|
args: '<user-id>',
|
||||||
if (matches) {
|
description: _td('Ignores a user, hiding their messages from you'),
|
||||||
const userId = matches[1];
|
runFn: function(roomId, args) {
|
||||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
if (args) {
|
||||||
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
const cli = MatrixClientPeg.get();
|
||||||
return success(
|
|
||||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
const matches = args.match(/^(\S+)$/);
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
if (matches) {
|
||||||
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
const userId = matches[1];
|
||||||
title: _t("Ignored user"),
|
const ignoredUsers = cli.getIgnoredUsers();
|
||||||
description: (
|
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
||||||
<div>
|
return success(
|
||||||
<p>{ _t("You are now ignoring %(userId)s", {userId: userId}) }</p>
|
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
||||||
</div>
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
),
|
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
||||||
hasCancelButton: false,
|
title: _t('Ignored user'),
|
||||||
});
|
description: <div>
|
||||||
}),
|
<p>{ _t('You are now ignoring %(userId)s', {userId}) }</p>
|
||||||
);
|
</div>,
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
unignore: new Command("unignore", "<userId>", function(roomId, args) {
|
unignore: new Command({
|
||||||
if (args) {
|
name: 'unignore',
|
||||||
const matches = args.match(/^(\S+)$/);
|
args: '<user-id>',
|
||||||
if (matches) {
|
description: _td('Stops ignoring a user, showing their messages going forward'),
|
||||||
const userId = matches[1];
|
runFn: function(roomId, args) {
|
||||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
if (args) {
|
||||||
const index = ignoredUsers.indexOf(userId);
|
const cli = MatrixClientPeg.get();
|
||||||
if (index !== -1) ignoredUsers.splice(index, 1);
|
|
||||||
return success(
|
const matches = args.match(/^(\S+)$/);
|
||||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
if (matches) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const userId = matches[1];
|
||||||
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
const ignoredUsers = cli.getIgnoredUsers();
|
||||||
title: _t("Unignored user"),
|
const index = ignoredUsers.indexOf(userId);
|
||||||
description: (
|
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||||
<div>
|
return success(
|
||||||
<p>{ _t("You are no longer ignoring %(userId)s", {userId: userId}) }</p>
|
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
||||||
</div>
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
),
|
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
||||||
hasCancelButton: false,
|
title: _t('Unignored user'),
|
||||||
});
|
description: <div>
|
||||||
}),
|
<p>{ _t('You are no longer ignoring %(userId)s', {userId}) }</p>
|
||||||
);
|
</div>,
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
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({
|
||||||
if (args) {
|
name: 'op',
|
||||||
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
args: '<user-id> [<power-level>]',
|
||||||
let powerLevel = 50; // default power level for op
|
description: _td('Define the power level of a user'),
|
||||||
if (matches) {
|
runFn: function(roomId, args) {
|
||||||
const userId = matches[1];
|
if (args) {
|
||||||
if (matches.length === 4 && undefined !== matches[3]) {
|
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
||||||
powerLevel = parseInt(matches[3]);
|
let powerLevel = 50; // default power level for op
|
||||||
}
|
if (matches) {
|
||||||
if (!isNaN(powerLevel)) {
|
const userId = matches[1];
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
if (matches.length === 4 && undefined !== matches[3]) {
|
||||||
if (!room) {
|
powerLevel = parseInt(matches[3]);
|
||||||
return reject("Bad room ID: " + roomId);
|
}
|
||||||
|
if (!isNaN(powerLevel)) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const room = cli.getRoom(roomId);
|
||||||
|
if (!room) return reject('Bad room ID: ' + roomId);
|
||||||
|
|
||||||
|
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||||
|
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
|
||||||
}
|
}
|
||||||
const powerLevelEvent = room.currentState.getStateEvents(
|
|
||||||
"m.room.power_levels", "",
|
|
||||||
);
|
|
||||||
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({
|
||||||
if (args) {
|
name: 'deop',
|
||||||
const matches = args.match(/^(\S+)$/);
|
args: '<user-id>',
|
||||||
if (matches) {
|
description: _td('Deops user with given id'),
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
runFn: function(roomId, args) {
|
||||||
if (!room) {
|
if (args) {
|
||||||
return reject("Bad room ID: " + roomId);
|
const matches = args.match(/^(\S+)$/);
|
||||||
}
|
if (matches) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const room = cli.getRoom(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'),
|
||||||
Modal.createDialog(DevtoolsDialog, { roomId });
|
runFn: function(roomId) {
|
||||||
return success();
|
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
||||||
|
Modal.createDialog(DevtoolsDialog, {roomId});
|
||||||
|
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({
|
||||||
if (args) {
|
name: 'verify',
|
||||||
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
|
args: '<user-id> <device-id> <device-signing-key>',
|
||||||
if (matches) {
|
description: _td('Verifies a user, device, and pubkey tuple'),
|
||||||
const userId = matches[1];
|
runFn: function(roomId, args) {
|
||||||
const deviceId = matches[2];
|
if (args) {
|
||||||
const fingerprint = matches[3];
|
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
return success(
|
const userId = matches[1];
|
||||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
const deviceId = matches[2];
|
||||||
// in future
|
const fingerprint = matches[3];
|
||||||
Promise.resolve(MatrixClientPeg.get().getStoredDevice(userId, deviceId)).then((device) => {
|
|
||||||
if (!device) {
|
|
||||||
throw new Error(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.isVerified()) {
|
return success(
|
||||||
if (device.getFingerprint() === fingerprint) {
|
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||||
throw new Error(_t(`Device already verified!`));
|
// in future
|
||||||
} else {
|
Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => {
|
||||||
throw new Error(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
|
if (!device) {
|
||||||
|
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (device.getFingerprint() !== fingerprint) {
|
if (device.isVerified()) {
|
||||||
const fprint = device.getFingerprint();
|
if (device.getFingerprint() === fingerprint) {
|
||||||
throw new Error(
|
throw new Error(_t('Device already verified!'));
|
||||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
} else {
|
||||||
' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
|
throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!'));
|
||||||
' "%(fingerprint)s". This could mean your communications are being intercepted!',
|
}
|
||||||
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true);
|
if (device.getFingerprint() !== fingerprint) {
|
||||||
}).then(() => {
|
const fprint = device.getFingerprint();
|
||||||
// Tell the user we verified everything
|
throw new Error(
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
||||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
|
||||||
title: _t("Verified key"),
|
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||||
description: (
|
{
|
||||||
<div>
|
fprint,
|
||||||
<p>
|
userId,
|
||||||
{
|
deviceId,
|
||||||
_t("The signing key you provided matches the signing key you received " +
|
fingerprint,
|
||||||
"from %(userId)s's device %(deviceId)s. Device marked as verified.",
|
}));
|
||||||
{userId: userId, deviceId: deviceId})
|
}
|
||||||
}
|
|
||||||
</p>
|
return cli.setDeviceVerified(userId, deviceId, true);
|
||||||
</div>
|
}).then(() => {
|
||||||
),
|
// Tell the user we verified everything
|
||||||
hasCancelButton: false,
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
});
|
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
||||||
}),
|
title: _t('Verified key'),
|
||||||
);
|
description: <div>
|
||||||
|
<p>
|
||||||
|
{
|
||||||
|
_t('The signing key you provided matches the signing key you received ' +
|
||||||
|
'from %(userId)s\'s device %(deviceId)s. Device marked as verified.',
|
||||||
|
{userId, deviceId})
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>,
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
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,50 +476,39 @@ 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.
|
* @param {string} input The raw text input by the user.
|
||||||
* @param {string} input The raw text input by the user.
|
* @return {Object|null} An object with the property 'error' if there was an error
|
||||||
* @return {Object|null} An object with the property 'error' if there was an error
|
* 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.
|
*/
|
||||||
*/
|
export function processCommandInput(roomId, input) {
|
||||||
processInput: function(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] !== '/' || input[1] === '/') return null; // not a command
|
||||||
if (input[0] === "/" && input[1] !== "/") {
|
|
||||||
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
|
||||||
let cmd;
|
|
||||||
let args;
|
|
||||||
if (bits) {
|
|
||||||
cmd = bits[1].substring(1).toLowerCase();
|
|
||||||
args = bits[3];
|
|
||||||
} else {
|
|
||||||
cmd = input;
|
|
||||||
}
|
|
||||||
if (cmd === "me") return null;
|
|
||||||
if (aliases[cmd]) {
|
|
||||||
cmd = aliases[cmd];
|
|
||||||
}
|
|
||||||
if (commands[cmd]) {
|
|
||||||
return commands[cmd].run(roomId, args);
|
|
||||||
} else {
|
|
||||||
return reject(_t("Unrecognised command:") + ' ' + input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null; // not a command
|
|
||||||
},
|
|
||||||
|
|
||||||
getCommandList: function() {
|
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
||||||
// Return all the commands plus /me and /markdown which aren't handled like normal commands
|
let cmd;
|
||||||
const cmds = Object.keys(commands).sort().map(function(cmdKey) {
|
let args;
|
||||||
return commands[cmdKey];
|
if (bits) {
|
||||||
});
|
cmd = bits[1].substring(1).toLowerCase();
|
||||||
cmds.push(new Command("me", "<action>", function() {}));
|
args = bits[3];
|
||||||
cmds.push(new Command("markdown", "<on|off>", function() {}));
|
} else {
|
||||||
|
cmd = input;
|
||||||
|
}
|
||||||
|
|
||||||
return cmds;
|
if (aliases[cmd]) {
|
||||||
},
|
cmd = aliases[cmd];
|
||||||
};
|
}
|
||||||
|
if (CommandMap[cmd]) {
|
||||||
|
// 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 {
|
||||||
|
return reject(_t('Unrecognised command:') + ' ' + input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,93 +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';
|
|
||||||
import SdkConfig from "./SdkConfig";
|
|
||||||
import * as url from "url";
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
|
||||||
* @param {[type]} testUrlString URL to check
|
|
||||||
* @return {Boolean} True if specified URL is a scalar URL
|
|
||||||
*/
|
|
||||||
static isScalarUrl(testUrlString) {
|
|
||||||
if (!testUrlString) {
|
|
||||||
console.error('Scalar URL check failed. No URL specified');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testUrl = url.parse(testUrlString);
|
|
||||||
|
|
||||||
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++) {
|
|
||||||
const scalarUrl = url.parse(scalarUrls[i]);
|
|
||||||
if (testUrl && scalarUrl) {
|
|
||||||
if (
|
|
||||||
testUrl.protocol === scalarUrl.protocol &&
|
|
||||||
testUrl.host === scalarUrl.host &&
|
|
||||||
testUrl.pathname.startsWith(scalarUrl.pathname)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
||||||
|
@ -36,7 +36,7 @@ export default class AutocompleteProvider {
|
||||||
/**
|
/**
|
||||||
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
|
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
|
||||||
*/
|
*/
|
||||||
getCurrentCommand(query: string, selection: {start: number, end: number}, force: boolean = false): ?string {
|
getCurrentCommand(query: string, selection: SelectionRange, force: boolean = false): ?string {
|
||||||
let commandRegex = this.commandRegex;
|
let commandRegex = this.commandRegex;
|
||||||
|
|
||||||
if (force && this.shouldForceComplete()) {
|
if (force && this.shouldForceComplete()) {
|
||||||
|
@ -51,14 +51,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';
|
||||||
|
@ -47,6 +49,7 @@ const PROVIDERS = [
|
||||||
EmojiProvider,
|
EmojiProvider,
|
||||||
NotifProvider,
|
NotifProvider,
|
||||||
CommandProvider,
|
CommandProvider,
|
||||||
|
CommunityProvider,
|
||||||
DuckDuckGoProvider,
|
DuckDuckGoProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -54,7 +57,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,103 +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 {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() {
|
||||||
|
@ -123,23 +37,36 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}) {
|
async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array<Completion> {
|
||||||
let completions = [];
|
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (!command) return [];
|
||||||
completions = this.matcher.match(command[0]).map((result) => {
|
|
||||||
return {
|
let matches = [];
|
||||||
completion: result.command + ' ',
|
if (command[0] !== command[1]) {
|
||||||
component: (<TextualCompletion
|
// The input looks like a command with arguments, perform exact match
|
||||||
title={result.command}
|
const name = command[1].substr(1); // strip leading `/`
|
||||||
subtitle={result.args}
|
if (CommandMap[name]) {
|
||||||
description={_t(result.description)}
|
matches = [CommandMap[name]];
|
||||||
/>),
|
}
|
||||||
range,
|
} else {
|
||||||
};
|
if (query === '/') {
|
||||||
});
|
// If they have just entered `/` show everything
|
||||||
|
matches = COMMANDS;
|
||||||
|
} else {
|
||||||
|
// otherwise fuzzy match against all of the fields
|
||||||
|
matches = this.matcher.match(command[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return completions;
|
|
||||||
|
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}
|
||||||
|
subtitle={result.args}
|
||||||
|
description={_t(result.description)} />,
|
||||||
|
range,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
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,6 +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";
|
||||||
|
|
||||||
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
||||||
const REFERRER = 'vector';
|
const REFERRER = 'vector';
|
||||||
|
@ -36,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: {start: number, end: number}) {
|
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,6 +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 {Completion, SelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const AT_ROOM_REGEX = /@\S*/g;
|
const AT_ROOM_REGEX = /@\S*/g;
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ export default class NotifProvider extends AutocompleteProvider {
|
||||||
this.room = room;
|
this.room = room;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}, 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,8 +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 {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);
|
||||||
|
@ -46,7 +48,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}, 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');
|
||||||
|
|
||||||
// Disable autocompletions when composing commands because of various issues
|
// Disable autocompletions when composing commands because of various issues
|
||||||
|
|
|
@ -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,20 +24,20 @@ 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 {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;
|
||||||
|
|
||||||
export default class UserProvider extends AutocompleteProvider {
|
export default class UserProvider extends AutocompleteProvider {
|
||||||
users: Array<RoomMember> = null;
|
users: Array<RoomMember> = null;
|
||||||
room: Room = null;
|
room: Room = null;
|
||||||
|
|
||||||
constructor(room) {
|
constructor(room: Room) {
|
||||||
super(USER_REGEX, {
|
super(USER_REGEX, {
|
||||||
keys: ['name'],
|
keys: ['name'],
|
||||||
});
|
});
|
||||||
|
@ -44,7 +45,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);
|
||||||
|
@ -61,7 +62,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;
|
||||||
|
@ -77,7 +78,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;
|
||||||
|
@ -87,7 +88,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
this.users = null;
|
this.users = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: {start: number, end: number}, 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');
|
||||||
|
|
||||||
// Disable autocompletions when composing commands because of various issues
|
// Disable autocompletions when composing commands because of various issues
|
||||||
|
@ -128,7 +129,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName(): string {
|
||||||
return '👥 ' + _t('Users');
|
return '👥 ' + _t('Users');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,13 +142,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",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -562,6 +562,13 @@ 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();
|
||||||
},
|
},
|
||||||
|
@ -1052,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') }
|
||||||
|
@ -1064,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') }
|
||||||
|
@ -1127,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;
|
||||||
|
@ -1207,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"
|
||||||
|
@ -1231,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"
|
||||||
|
@ -1269,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,8 +149,7 @@ 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") ||
|
||||||
classes.contains("mx_RoomSubList_ellipsis")));
|
classes.contains("mx_RoomSubList_ellipsis")));
|
||||||
|
|
|
@ -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";
|
||||||
|
@ -1143,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,
|
||||||
|
@ -1308,6 +1304,21 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dft = new DecryptionFailureTracker((total) => {
|
||||||
|
// TODO: Pass reason for failure as third argument to trackEvent
|
||||||
|
Analytics.trackEvent('E2E', 'Decryption failure', null, total);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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) => dft.eventDecrypted(e));
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -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,11 +351,13 @@ 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 className="mx_RoomStatusBar_connectionLostBar_title">
|
<div>
|
||||||
{ title }
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
</div>
|
{ title }
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
</div>
|
||||||
{ content }
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
|
{ content }
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
@ -350,11 +374,13 @@ 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 className="mx_RoomStatusBar_connectionLostBar_title">
|
<div>
|
||||||
{ _t('Connectivity to the server has been lost.') }
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
</div>
|
{ _t('Connectivity to the server has been lost.') }
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
</div>
|
||||||
{ _t('Sent messages will be stored until your connection has returned.') }
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
|
{ _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,30 +16,25 @@ 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';
|
||||||
|
|
||||||
|
|
||||||
// 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,
|
||||||
|
@ -77,8 +73,10 @@ 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,
|
||||||
};
|
};
|
||||||
|
@ -105,15 +103,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,12 +139,12 @@ 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) {
|
||||||
// as good a way as any to reset the truncate state
|
// as good a way as any to reset the truncate state
|
||||||
this.setState({ truncateAt : TRUNCATE_AT });
|
this.setState({truncateAt: TRUNCATE_AT});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onShowMoreRooms();
|
this.props.onShowMoreRooms();
|
||||||
|
@ -159,7 +159,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 +169,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 +187,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 +238,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,63 +322,66 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_showFullMemberList: function() {
|
_showFullMemberList: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
truncateAt: -1
|
truncateAt: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.onShowMoreRooms();
|
this.props.onShowMoreRooms();
|
||||||
|
@ -341,37 +389,39 @@ 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.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
if (this.state.sortedList.length === 0 && this.props.extraTiles.length === 0) {
|
||||||
content = this.props.emptyContent;
|
// 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 {
|
} else {
|
||||||
content = this.makeRoomTiles();
|
content = this.makeRoomTiles();
|
||||||
content.push(...this.props.extraTiles);
|
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>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const subListContent = <div>
|
const subListContent = <div>
|
||||||
{ this._getHeaderJsx() }
|
{this._getHeaderJsx()}
|
||||||
{ subList }
|
{subList}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
return this.props.editable ?
|
return this.props.editable ?
|
||||||
|
@ -379,23 +429,26 @@ var RoomSubList = React.createClass({
|
||||||
droppableId={"room-sub-list-droppable_" + this.props.tagName}
|
droppableId={"room-sub-list-droppable_" + this.props.tagName}
|
||||||
type="draggable-RoomTile"
|
type="draggable-RoomTile"
|
||||||
>
|
>
|
||||||
{ (provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div ref={provided.innerRef}>
|
<div ref={provided.innerRef}>
|
||||||
{ subListContent }
|
{subListContent}
|
||||||
</div>
|
</div>
|
||||||
) }
|
)}
|
||||||
</Droppable> : subListContent;
|
</Droppable> : subListContent;
|
||||||
}
|
} else {
|
||||||
else {
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
if (this.props.showSpinner) {
|
||||||
|
content = <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
||||||
|
@ -45,6 +46,7 @@ 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, {SettingLevel} 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() {};
|
||||||
|
@ -317,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() {
|
||||||
|
@ -618,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),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -645,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -913,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;
|
||||||
|
|
|
@ -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) {
|
||||||
dis.dispatch({action: 'deselect_tags'});
|
// only dispatch if its not a no-op
|
||||||
|
if (this.state.selectedTags.length > 0) {
|
||||||
|
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;
|
||||||
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
|
||||||
alt={_t("Clear filter")}
|
|
||||||
title={_t("Clear filter")}
|
|
||||||
/> :
|
|
||||||
<div />;
|
|
||||||
|
|
||||||
return <div className="mx_TagPanel">
|
let clearButton;
|
||||||
<AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
if (itemsSelected) {
|
||||||
|
clearButton = <AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||||
|
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
||||||
|
alt={_t("Clear filter")}
|
||||||
|
title={_t("Clear filter")}
|
||||||
|
/>
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = classNames('mx_TagPanel', {
|
||||||
|
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"
|
||||||
|
|
|
@ -298,6 +298,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'),
|
||||||
});
|
});
|
||||||
|
@ -428,7 +429,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) {
|
||||||
|
@ -976,6 +976,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);
|
||||||
|
@ -1016,6 +1021,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>;
|
||||||
|
|
||||||
|
@ -1024,6 +1030,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 = '';
|
||||||
|
@ -1065,8 +1091,9 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
{ microphoneDropdown }
|
{ speakerDropdown }
|
||||||
{ webcamDropdown }
|
{ microphoneDropdown }
|
||||||
|
{ webcamDropdown }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1080,6 +1107,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');
|
||||||
|
@ -1301,10 +1336,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. " +
|
||||||
_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>.", {},
|
||||||
"Either use HTTPS or <a>enable unsafe scripts</a>.",
|
{
|
||||||
{},
|
'a': (sub) => {
|
||||||
{ 'a': (sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; } },
|
return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">
|
||||||
|
{ sub }
|
||||||
|
</a>;
|
||||||
|
},
|
||||||
|
},
|
||||||
) }
|
) }
|
||||||
</span>;
|
</span>;
|
||||||
} else {
|
} else {
|
||||||
errorText = <span>
|
errorText = <span>
|
||||||
{
|
{ _t("Can't connect to homeserver - please check your connectivity, ensure your " +
|
||||||
_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>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");
|
||||||
|
@ -403,21 +401,10 @@ module.exports = React.createClass({
|
||||||
</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 />
|
||||||
|
|
||||||
|
|
|
@ -67,9 +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')}
|
||||||
cancelButton={this.props.cancelButton}
|
|
||||||
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,7 +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 "../../../WidgetUtils";
|
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||||
|
|
||||||
export default class AppPermission extends React.Component {
|
export default class AppPermission extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ 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:'];
|
||||||
|
@ -319,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);
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,7 +36,7 @@ function getOrCreateContainer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Greater than that of the ContextualMenu
|
// Greater than that of the ContextualMenu
|
||||||
const PE_Z_INDEX = 3000;
|
const PE_Z_INDEX = 5000;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -81,12 +84,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;
|
||||||
|
@ -109,9 +114,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: {
|
||||||
|
@ -140,8 +147,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() {
|
||||||
|
@ -179,6 +199,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');
|
||||||
|
|
||||||
|
@ -229,6 +250,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,
|
onContextMenu: function(e) {
|
||||||
top: y,
|
e.preventDefault();
|
||||||
tag: this.props.tag,
|
|
||||||
onFinished: function() {
|
const chevronOffset = 12;
|
||||||
self.setState({ menuDisplayed: false });
|
this._openContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||||
},
|
|
||||||
});
|
|
||||||
this.setState({ menuDisplayed: true });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
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}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -187,7 +187,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberInfo">
|
<div className="mx_MemberInfo">
|
||||||
<GeminiScrollbarWrapper autoshow={true}>
|
<GeminiScrollbarWrapper autoshow={true}>
|
||||||
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
|
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
|
||||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<div className="mx_MemberInfo_avatar">
|
<div className="mx_MemberInfo_avatar">
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -180,6 +217,17 @@ class PasswordLogin extends React.Component {
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 }),
|
||||||
}, "*");
|
}, "*");
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 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.
|
||||||
|
@ -15,25 +16,19 @@ 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 { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
|
||||||
import MFileBody from './MFileBody';
|
import MFileBody from './MFileBody';
|
||||||
import ImageUtils from '../../../ImageUtils';
|
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
|
||||||
import { decryptFile } from '../../../utils/DecryptFile';
|
import { decryptFile } from '../../../utils/DecryptFile';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
export default class extends React.Component {
|
export default class MImageBody extends React.Component {
|
||||||
displayName: 'MImageBody'
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
/* the MatrixEvent to show */
|
/* the MatrixEvent to show */
|
||||||
mxEvent: PropTypes.object.isRequired,
|
mxEvent: PropTypes.object.isRequired,
|
||||||
|
@ -43,23 +38,21 @@ export default class extends React.Component {
|
||||||
|
|
||||||
/* the maximum image height to use */
|
/* the maximum image height to use */
|
||||||
maxImageHeight: PropTypes.number,
|
maxImageHeight: PropTypes.number,
|
||||||
}
|
};
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.onAction = this.onAction.bind(this);
|
|
||||||
this.onImageError = this.onImageError.bind(this);
|
this.onImageError = this.onImageError.bind(this);
|
||||||
this.onImageLoad = this.onImageLoad.bind(this);
|
this.onImageLoad = this.onImageLoad.bind(this);
|
||||||
this.onImageEnter = this.onImageEnter.bind(this);
|
this.onImageEnter = this.onImageEnter.bind(this);
|
||||||
this.onImageLeave = this.onImageLeave.bind(this);
|
this.onImageLeave = this.onImageLeave.bind(this);
|
||||||
this.onClientSync = this.onClientSync.bind(this);
|
this.onClientSync = this.onClientSync.bind(this);
|
||||||
this.onClick = this.onClick.bind(this);
|
this.onClick = this.onClick.bind(this);
|
||||||
this.fixupHeight = this.fixupHeight.bind(this);
|
|
||||||
this._isGif = this._isGif.bind(this);
|
this._isGif = this._isGif.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -68,6 +61,9 @@ export default class extends React.Component {
|
||||||
decryptedBlob: null,
|
decryptedBlob: null,
|
||||||
error: null,
|
error: null,
|
||||||
imgError: false,
|
imgError: false,
|
||||||
|
imgLoaded: false,
|
||||||
|
loadedImageDimensions: null,
|
||||||
|
hover: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +87,7 @@ export default class extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(ev) {
|
onClick(ev) {
|
||||||
if (ev.button == 0 && !ev.metaKey) {
|
if (ev.button === 0 && !ev.metaKey) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
const httpUrl = this._getContentUrl();
|
const httpUrl = this._getContentUrl();
|
||||||
|
@ -122,6 +118,8 @@ export default class extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onImageEnter(e) {
|
onImageEnter(e) {
|
||||||
|
this.setState({ hover: true });
|
||||||
|
|
||||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -130,6 +128,8 @@ export default class extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onImageLeave(e) {
|
onImageLeave(e) {
|
||||||
|
this.setState({ hover: false });
|
||||||
|
|
||||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -145,6 +145,16 @@ export default class extends React.Component {
|
||||||
|
|
||||||
onImageLoad() {
|
onImageLoad() {
|
||||||
this.props.onWidgetLoad();
|
this.props.onWidgetLoad();
|
||||||
|
|
||||||
|
let loadedImageDimensions;
|
||||||
|
|
||||||
|
if (this.refs.image) {
|
||||||
|
const { naturalWidth, naturalHeight } = this.refs.image;
|
||||||
|
|
||||||
|
loadedImageDimensions = { naturalWidth, naturalHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ imgLoaded: true, loadedImageDimensions });
|
||||||
}
|
}
|
||||||
|
|
||||||
_getContentUrl() {
|
_getContentUrl() {
|
||||||
|
@ -164,9 +174,7 @@ export default class extends React.Component {
|
||||||
return this.state.decryptedThumbnailUrl;
|
return this.state.decryptedThumbnailUrl;
|
||||||
}
|
}
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else if (content.info &&
|
} else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) {
|
||||||
content.info.mimetype == "image/svg+xml" &&
|
|
||||||
content.info.thumbnail_url) {
|
|
||||||
// special case to return client-generated thumbnails for SVGs, if any,
|
// special case to return client-generated thumbnails for SVGs, if any,
|
||||||
// given we deliberately don't thumbnail them serverside to prevent
|
// given we deliberately don't thumbnail them serverside to prevent
|
||||||
// billion lol attacks and similar
|
// billion lol attacks and similar
|
||||||
|
@ -179,11 +187,10 @@ export default class extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
let thumbnailPromise = Promise.resolve(null);
|
let thumbnailPromise = Promise.resolve(null);
|
||||||
if (content.info.thumbnail_file) {
|
if (content.info && content.info.thumbnail_file) {
|
||||||
thumbnailPromise = decryptFile(
|
thumbnailPromise = decryptFile(
|
||||||
content.info.thumbnail_file,
|
content.info.thumbnail_file,
|
||||||
).then(function(blob) {
|
).then(function(blob) {
|
||||||
|
@ -210,7 +217,6 @@ export default class extends React.Component {
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
}
|
}
|
||||||
this.fixupHeight();
|
|
||||||
this._afterComponentDidMount();
|
this._afterComponentDidMount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +227,6 @@ export default class extends React.Component {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
dis.unregister(this.dispatcherRef);
|
|
||||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||||
this._afterComponentWillUnmount();
|
this._afterComponentWillUnmount();
|
||||||
|
|
||||||
|
@ -238,60 +243,113 @@ export default class extends React.Component {
|
||||||
_afterComponentWillUnmount() {
|
_afterComponentWillUnmount() {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction(payload) {
|
|
||||||
if (payload.action === "timeline_resize") {
|
|
||||||
this.fixupHeight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixupHeight() {
|
|
||||||
if (!this.refs.image) {
|
|
||||||
console.warn(`Refusing to fix up height on ${this.displayName} with no image element`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = this.props.mxEvent.getContent();
|
|
||||||
const timelineWidth = this.refs.body.offsetWidth;
|
|
||||||
const maxHeight = this.props.maxImageHeight || 600; // let images take up as much width as they can so long
|
|
||||||
// as the height doesn't exceed 600px. The alternative here would be 600*timelineWidth/800; to scale them down
|
|
||||||
// to fit inside a 4:3 bounding box
|
|
||||||
|
|
||||||
// FIXME: this will break on clientside generated thumbnails (as per e2e rooms)
|
|
||||||
// which may well be much smaller than the 800x600 bounding box.
|
|
||||||
|
|
||||||
// FIXME: It will also break really badly for images with broken or missing thumbnails
|
|
||||||
|
|
||||||
// FIXME: Because we don't know what size of thumbnail the server's actually going to send
|
|
||||||
// us, we can't even really layout the page nicely for it. Instead we have to assume
|
|
||||||
// it'll target 800x600 and we'll downsize if needed to make things fit.
|
|
||||||
|
|
||||||
// console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
|
|
||||||
let thumbHeight = null;
|
|
||||||
if (content.info) {
|
|
||||||
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
|
|
||||||
}
|
|
||||||
this.refs.image.style.height = thumbHeight + "px";
|
|
||||||
// console.log("Image height now", thumbHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
_messageContent(contentUrl, thumbUrl, content) {
|
_messageContent(contentUrl, thumbUrl, content) {
|
||||||
|
let infoWidth;
|
||||||
|
let infoHeight;
|
||||||
|
|
||||||
|
if (content && content.info && content.info.w && content.info.h) {
|
||||||
|
infoWidth = content.info.w;
|
||||||
|
infoHeight = content.info.h;
|
||||||
|
} else {
|
||||||
|
// Whilst the image loads, display nothing.
|
||||||
|
//
|
||||||
|
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
|
||||||
|
//
|
||||||
|
// By doing this, the image "pops" into the timeline, but is still restricted
|
||||||
|
// by the same width and height logic below.
|
||||||
|
if (!this.state.loadedImageDimensions) {
|
||||||
|
return this.wrapImage(contentUrl,
|
||||||
|
<img style={{display: 'none'}} src={thumbUrl} ref="image"
|
||||||
|
alt={content.body}
|
||||||
|
onError={this.onImageError}
|
||||||
|
onLoad={this.onImageLoad}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
infoWidth = this.state.loadedImageDimensions.naturalWidth;
|
||||||
|
infoHeight = this.state.loadedImageDimensions.naturalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The maximum height of the thumbnail as it is rendered as an <img>
|
||||||
|
const maxHeight = Math.min(this.props.maxImageHeight || 600, infoHeight);
|
||||||
|
// The maximum width of the thumbnail, as dictated by its natural
|
||||||
|
// maximum height.
|
||||||
|
const maxWidth = infoWidth * maxHeight / infoHeight;
|
||||||
|
|
||||||
|
let img = null;
|
||||||
|
let placeholder = null;
|
||||||
|
|
||||||
|
// e2e image hasn't been decrypted yet
|
||||||
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
|
placeholder = <img src="img/spinner.gif" alt={content.body} width="32" height="32" />;
|
||||||
|
} else if (!this.state.imgLoaded) {
|
||||||
|
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
||||||
|
placeholder = this.getPlaceholder();
|
||||||
|
}
|
||||||
|
|
||||||
|
const showPlaceholder = Boolean(placeholder);
|
||||||
|
|
||||||
|
if (thumbUrl && !this.state.imgError) {
|
||||||
|
// Restrict the width of the thumbnail here, otherwise it will fill the container
|
||||||
|
// which has the same width as the timeline
|
||||||
|
// mx_MImageBody_thumbnail resizes img to exactly container size
|
||||||
|
img = <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||||
|
style={{ maxWidth: maxWidth + "px" }}
|
||||||
|
alt={content.body}
|
||||||
|
onError={this.onImageError}
|
||||||
|
onLoad={this.onImageLoad}
|
||||||
|
onMouseEnter={this.onImageEnter}
|
||||||
|
onMouseLeave={this.onImageLeave} />;
|
||||||
|
}
|
||||||
|
|
||||||
const thumbnail = (
|
const thumbnail = (
|
||||||
<a href={contentUrl} onClick={this.onClick}>
|
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px" }} >
|
||||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
{ /* Calculate aspect ratio, using %padding will size _container correctly */ }
|
||||||
alt={content.body}
|
<div style={{ paddingBottom: (100 * infoHeight / infoWidth) + '%' }} />
|
||||||
onError={this.onImageError}
|
|
||||||
onLoad={this.onImageLoad}
|
{ showPlaceholder &&
|
||||||
onMouseEnter={this.onImageEnter}
|
<div className="mx_MImageBody_thumbnail" style={{
|
||||||
onMouseLeave={this.onImageLeave} />
|
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||||
</a>
|
maxWidth: infoWidth + "px",
|
||||||
|
}}>
|
||||||
|
<div className="mx_MImageBody_thumbnail_spinner">
|
||||||
|
{ placeholder }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div style={{display: !showPlaceholder ? undefined : 'none'}}>
|
||||||
|
{ img }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ this.state.hover && this.getTooltip() }
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return this.wrapImage(contentUrl, thumbnail);
|
||||||
<span className="mx_MImageBody" ref="body">
|
}
|
||||||
{ thumbUrl && !this.state.imgError ? thumbnail : '' }
|
|
||||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
// Overidden by MStickerBody
|
||||||
</span>
|
wrapImage(contentUrl, children) {
|
||||||
);
|
return <a href={contentUrl} onClick={this.onClick}>
|
||||||
|
{children}
|
||||||
|
</a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overidden by MStickerBody
|
||||||
|
getPlaceholder() {
|
||||||
|
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overidden by MStickerBody
|
||||||
|
getTooltip() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overidden by MStickerBody
|
||||||
|
getFileBody() {
|
||||||
|
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -306,25 +364,6 @@ export default class extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
|
||||||
// Need to decrypt the attachment
|
|
||||||
// The attachment is decrypted in componentDidMount.
|
|
||||||
// For now add an img tag with a spinner.
|
|
||||||
return (
|
|
||||||
<span className="mx_MImageBody" ref="body">
|
|
||||||
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
|
||||||
"display": "flex",
|
|
||||||
"alignItems": "center",
|
|
||||||
"width": "100%",
|
|
||||||
}}>
|
|
||||||
<img src="img/spinner.gif" alt={content.body} width="32" height="32" style={{
|
|
||||||
"margin": "auto",
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentUrl = this._getContentUrl();
|
const contentUrl = this._getContentUrl();
|
||||||
let thumbUrl;
|
let thumbUrl;
|
||||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||||
|
@ -333,6 +372,12 @@ export default class extends React.Component {
|
||||||
thumbUrl = this._getThumbUrl();
|
thumbUrl = this._getThumbUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._messageContent(contentUrl, thumbUrl, content);
|
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
|
||||||
|
const fileBody = this.getFileBody();
|
||||||
|
|
||||||
|
return <span className="mx_MImageBody" ref="body">
|
||||||
|
{ thumbnail }
|
||||||
|
{ fileBody }
|
||||||
|
</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,143 +18,39 @@ limitations under the License.
|
||||||
|
|
||||||
import MImageBody from './MImageBody';
|
import MImageBody from './MImageBody';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import TintableSVG from '../elements/TintableSvg';
|
|
||||||
|
|
||||||
export default class MStickerBody extends MImageBody {
|
export default class MStickerBody extends MImageBody {
|
||||||
displayName: 'MStickerBody'
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
|
||||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
|
||||||
this._onImageLoad = this._onImageLoad.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onMouseEnter() {
|
|
||||||
this.setState({showTooltip: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
_onMouseLeave() {
|
|
||||||
this.setState({showTooltip: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
_onImageLoad() {
|
|
||||||
this.setState({
|
|
||||||
placeholderClasses: 'mx_MStickerBody_placeholder_invisible',
|
|
||||||
});
|
|
||||||
const hidePlaceholderTimer = setTimeout(() => {
|
|
||||||
this.setState({
|
|
||||||
placeholderVisible: false,
|
|
||||||
imageClasses: 'mx_MStickerBody_image_visible',
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
this.setState({hidePlaceholderTimer});
|
|
||||||
if (this.props.onWidgetLoad) {
|
|
||||||
this.props.onWidgetLoad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_afterComponentDidMount() {
|
|
||||||
if (this.refs.image.complete) {
|
|
||||||
// Image already loaded
|
|
||||||
this.setState({
|
|
||||||
placeholderVisible: false,
|
|
||||||
placeholderClasses: '.mx_MStickerBody_placeholder_invisible',
|
|
||||||
imageClasses: 'mx_MStickerBody_image_visible',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Image not already loaded
|
|
||||||
this.setState({
|
|
||||||
placeholderVisible: true,
|
|
||||||
placeholderClasses: '',
|
|
||||||
imageClasses: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_afterComponentWillUnmount() {
|
|
||||||
if (this.state.hidePlaceholderTimer) {
|
|
||||||
clearTimeout(this.state.hidePlaceholderTimer);
|
|
||||||
this.setState({hidePlaceholderTimer: null});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_messageContent(contentUrl, thumbUrl, content) {
|
|
||||||
let tooltip;
|
|
||||||
const tooltipBody = (
|
|
||||||
this.props.mxEvent &&
|
|
||||||
this.props.mxEvent.getContent() &&
|
|
||||||
this.props.mxEvent.getContent().body) ?
|
|
||||||
this.props.mxEvent.getContent().body : null;
|
|
||||||
if (this.state.showTooltip && tooltipBody) {
|
|
||||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
|
||||||
tooltip = <RoomTooltip
|
|
||||||
className='mx_RoleButton_tooltip'
|
|
||||||
label={tooltipBody} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gutterSize = 0;
|
|
||||||
let placeholderSize = 75;
|
|
||||||
let placeholderFixupHeight = '100px';
|
|
||||||
let placeholderTop = 0;
|
|
||||||
let placeholderLeft = 0;
|
|
||||||
|
|
||||||
if (content.info) {
|
|
||||||
placeholderTop = Math.floor((content.info.h/2) - (placeholderSize/2)) + 'px';
|
|
||||||
placeholderLeft = Math.floor((content.info.w/2) - (placeholderSize/2) + gutterSize) + 'px';
|
|
||||||
placeholderFixupHeight = content.info.h + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
// The pixel size of sticker images is generally larger than their intended display
|
|
||||||
// size so they render at native reolution on HiDPI displays. We therefore need to
|
|
||||||
// explicity set the size so they render at the intended size.
|
|
||||||
// XXX: This will be clobberred when we run fixupHeight(), but we need to do it
|
|
||||||
// here otherwise the stickers are momentarily displayed at the pixel size.
|
|
||||||
const imageStyle = {
|
|
||||||
height: content.info.h,
|
|
||||||
// leave the browser the calculate the width automatically
|
|
||||||
};
|
|
||||||
|
|
||||||
placeholderSize = placeholderSize + 'px';
|
|
||||||
|
|
||||||
// Body 'ref' required by MImageBody
|
|
||||||
return (
|
|
||||||
<span className='mx_MStickerBody' ref='body'
|
|
||||||
style={{
|
|
||||||
height: placeholderFixupHeight,
|
|
||||||
}}>
|
|
||||||
<div className={'mx_MStickerBody_image_container'}>
|
|
||||||
{ this.state.placeholderVisible &&
|
|
||||||
<div
|
|
||||||
className={'mx_MStickerBody_placeholder ' + this.state.placeholderClasses}
|
|
||||||
style={{
|
|
||||||
top: placeholderTop,
|
|
||||||
left: placeholderLeft,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TintableSVG
|
|
||||||
src={'img/icons-show-stickers.svg'}
|
|
||||||
width={placeholderSize}
|
|
||||||
height={placeholderSize} />
|
|
||||||
</div> }
|
|
||||||
<img
|
|
||||||
className={'mx_MStickerBody_image ' + this.state.imageClasses}
|
|
||||||
src={contentUrl}
|
|
||||||
style={imageStyle}
|
|
||||||
ref='image'
|
|
||||||
alt={content.body}
|
|
||||||
onLoad={this._onImageLoad}
|
|
||||||
onMouseEnter={this._onMouseEnter}
|
|
||||||
onMouseLeave={this._onMouseLeave}
|
|
||||||
/>
|
|
||||||
{ tooltip }
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty to prevent default behaviour of MImageBody
|
// Empty to prevent default behaviour of MImageBody
|
||||||
onClick() {
|
onClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
|
||||||
|
// which is added by mx_MStickerBody_wrapper
|
||||||
|
wrapImage(contentUrl, children) {
|
||||||
|
return <div className="mx_MStickerBody_wrapper"> { children } </div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder to show in place of the sticker image if
|
||||||
|
// img onLoad hasn't fired yet.
|
||||||
|
getPlaceholder() {
|
||||||
|
const TintableSVG = sdk.getComponent('elements.TintableSvg');
|
||||||
|
return <TintableSVG src="img/icons-show-stickers.svg" width="75" height="75" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip to show on mouse over
|
||||||
|
getTooltip() {
|
||||||
|
const content = this.props.mxEvent && this.props.mxEvent.getContent();
|
||||||
|
|
||||||
|
if (!content || !content.body || !content.info || !content.info.w) return null;
|
||||||
|
|
||||||
|
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||||
|
return <div style={{left: content.info.w + 'px'}} className="mx_MStickerBody_tooltip">
|
||||||
|
<RoomTooltip label={content.body} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show "Download this_file.png ..."
|
||||||
|
getFileBody() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,12 +147,7 @@ module.exports = React.createClass({
|
||||||
// For now add an img tag with a spinner.
|
// For now add an img tag with a spinner.
|
||||||
return (
|
return (
|
||||||
<span className="mx_MVideoBody" ref="body">
|
<span className="mx_MVideoBody" ref="body">
|
||||||
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner" ref="image">
|
||||||
"display": "flex",
|
|
||||||
"align-items": "center",
|
|
||||||
"justify-items": "center",
|
|
||||||
"width": "100%",
|
|
||||||
}}>
|
|
||||||
<img src="img/spinner.gif" alt={content.body} width="16" height="16" />
|
<img src="img/spinner.gif" alt={content.body} width="16" height="16" />
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -62,17 +62,24 @@ module.exports = React.createClass({
|
||||||
'm.audio': sdk.getComponent('messages.MAudioBody'),
|
'm.audio': sdk.getComponent('messages.MAudioBody'),
|
||||||
'm.video': sdk.getComponent('messages.MVideoBody'),
|
'm.video': sdk.getComponent('messages.MVideoBody'),
|
||||||
};
|
};
|
||||||
|
const evTypes = {
|
||||||
|
'm.sticker': sdk.getComponent('messages.MStickerBody'),
|
||||||
|
};
|
||||||
|
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
const type = this.props.mxEvent.getType();
|
||||||
const msgtype = content.msgtype;
|
const msgtype = content.msgtype;
|
||||||
let BodyType = UnknownBody;
|
let BodyType = UnknownBody;
|
||||||
if (msgtype && bodyTypes[msgtype]) {
|
if (!this.props.mxEvent.isRedacted()) {
|
||||||
BodyType = bodyTypes[msgtype];
|
// only resolve BodyType if event is not redacted
|
||||||
} else if (this.props.mxEvent.getType() === 'm.sticker') {
|
if (type && evTypes[type]) {
|
||||||
BodyType = sdk.getComponent('messages.MStickerBody');
|
BodyType = evTypes[type];
|
||||||
} else if (content.url) {
|
} else if (msgtype && bodyTypes[msgtype]) {
|
||||||
// Fallback to MFileBody if there's a content URL
|
BodyType = bodyTypes[msgtype];
|
||||||
BodyType = bodyTypes['m.file'];
|
} else if (content.url) {
|
||||||
|
// Fallback to MFileBody if there's a content URL
|
||||||
|
BodyType = bodyTypes['m.file'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BodyType
|
return <BodyType
|
||||||
|
|
|
@ -72,14 +72,12 @@ export default React.createClass({
|
||||||
|
|
||||||
_updateRelatedGroups() {
|
_updateRelatedGroups() {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
const relatedGroupsEvent = this.context.matrixClient
|
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||||
.getRoom(this.props.mxEvent.getRoomId())
|
if (!room) return;
|
||||||
.currentState
|
|
||||||
.getStateEvents('m.room.related_groups', '');
|
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
|
||||||
this.setState({
|
this.setState({
|
||||||
relatedGroups: relatedGroupsEvent ?
|
relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [],
|
||||||
relatedGroupsEvent.getContent().groups || []
|
|
||||||
: [],
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ import * as ContextualMenu from '../../structures/ContextualMenu';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
|
import {host as matrixtoHost} from '../../../matrix-to';
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -304,7 +305,7 @@ module.exports = React.createClass({
|
||||||
// never preview matrix.to links (if anything we should give a smart
|
// never preview matrix.to links (if anything we should give a smart
|
||||||
// preview of the room/user they point to: nobody needs to be reminded
|
// preview of the room/user they point to: nobody needs to be reminded
|
||||||
// what the matrix.to site looks like).
|
// what the matrix.to site looks like).
|
||||||
if (host == 'matrix.to') return false;
|
if (host === matrixtoHost) return false;
|
||||||
|
|
||||||
if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) {
|
if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) {
|
||||||
// it's a "foo.pl" style link
|
// it's a "foo.pl" style link
|
||||||
|
@ -336,10 +337,21 @@ module.exports = React.createClass({
|
||||||
left: x,
|
left: x,
|
||||||
top: y,
|
top: y,
|
||||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||||
});
|
}, false);
|
||||||
e.target.onmouseout = close;
|
e.target.onmouseleave = close;
|
||||||
};
|
};
|
||||||
p.appendChild(button);
|
|
||||||
|
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||||
|
// when the <pre> overflows and is scrolled horizontally.
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "mx_EventTile_pre_container";
|
||||||
|
|
||||||
|
// Insert containing div in place of <pre> block
|
||||||
|
p.parentNode.replaceChild(div, p);
|
||||||
|
|
||||||
|
// Append <pre> block and copy button to container
|
||||||
|
div.appendChild(p);
|
||||||
|
div.appendChild(button);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -422,8 +434,7 @@ module.exports = React.createClass({
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
const content = mxEvent.getContent();
|
const content = mxEvent.getContent();
|
||||||
|
|
||||||
const stripReply = SettingsStore.isFeatureEnabled("feature_rich_quoting") &&
|
const stripReply = ReplyThread.getParentEventId(mxEvent);
|
||||||
ReplyThread.getParentEventId(mxEvent);
|
|
||||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||||
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
||||||
// Part of Replies fallback support
|
// Part of Replies fallback support
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Travis Ralston
|
Copyright 2017 Travis Ralston
|
||||||
|
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,6 +16,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {MatrixClient} from "matrix-js-sdk";
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
const sdk = require("../../../index");
|
const sdk = require("../../../index");
|
||||||
|
@ -29,6 +31,10 @@ module.exports = React.createClass({
|
||||||
room: PropTypes.object,
|
room: PropTypes.object,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
contextTypes: {
|
||||||
|
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
saveSettings: function() {
|
saveSettings: function() {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
|
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
|
||||||
|
@ -39,42 +45,58 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
|
const isEncrypted = this.context.matrixClient.isRoomEncrypted(roomId);
|
||||||
|
|
||||||
let previewsForAccount = null;
|
let previewsForAccount = null;
|
||||||
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
|
|
||||||
previewsForAccount = (
|
|
||||||
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
previewsForAccount = (
|
|
||||||
_t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let previewsForRoom = null;
|
let previewsForRoom = null;
|
||||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
|
||||||
previewsForRoom = (
|
if (!isEncrypted) {
|
||||||
<label>
|
// Only show account setting state and room state setting state in non-e2ee rooms where they apply
|
||||||
<SettingsFlag name="urlPreviewsEnabled"
|
const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
|
||||||
level={SettingLevel.ROOM}
|
if (accountEnabled) {
|
||||||
roomId={this.props.room.roomId}
|
previewsForAccount = (
|
||||||
isExplicit={true}
|
_t("You have <a>enabled</a> URL previews by default.", {}, {
|
||||||
manualSave={true}
|
'a': (sub)=><a href="#/settings">{ sub }</a>,
|
||||||
ref="urlPreviewsRoom" />
|
})
|
||||||
</label>
|
);
|
||||||
);
|
} else if (accountEnabled) {
|
||||||
} else {
|
previewsForAccount = (
|
||||||
let str = _td("URL previews are enabled by default for participants in this room.");
|
_t("You have <a>disabled</a> URL previews by default.", {}, {
|
||||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
|
'a': (sub)=><a href="#/settings">{ sub }</a>,
|
||||||
str = _td("URL previews are disabled by default for participants in this room.");
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
previewsForRoom = (<label>{ _t(str) }</label>);
|
|
||||||
|
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||||
|
previewsForRoom = (
|
||||||
|
<label>
|
||||||
|
<SettingsFlag name="urlPreviewsEnabled"
|
||||||
|
level={SettingLevel.ROOM}
|
||||||
|
roomId={roomId}
|
||||||
|
isExplicit={true}
|
||||||
|
manualSave={true}
|
||||||
|
ref="urlPreviewsRoom" />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let str = _td("URL previews are enabled by default for participants in this room.");
|
||||||
|
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
|
||||||
|
str = _td("URL previews are disabled by default for participants in this room.");
|
||||||
|
}
|
||||||
|
previewsForRoom = (<label>{ _t(str) }</label>);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
previewsForAccount = (
|
||||||
|
_t("In encrypted rooms, like this one, URL previews are disabled by default to ensure that your " +
|
||||||
|
"homeserver (where the previews are generated) cannot gather information about links you see in " +
|
||||||
|
"this room.")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewsForRoomAccount = (
|
const previewsForRoomAccount = ( // in an e2ee room we use a special key to enforce per-room opt-in
|
||||||
<SettingsFlag name="urlPreviewsEnabled"
|
<SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'}
|
||||||
level={SettingLevel.ROOM_ACCOUNT}
|
level={SettingLevel.ROOM_ACCOUNT}
|
||||||
roomId={this.props.room.roomId}
|
roomId={roomId}
|
||||||
manualSave={true}
|
manualSave={true}
|
||||||
ref="urlPreviewsSelf"
|
ref="urlPreviewsSelf"
|
||||||
/>
|
/>
|
||||||
|
@ -83,8 +105,13 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomSettings_toggles">
|
<div className="mx_RoomSettings_toggles">
|
||||||
<h3>{ _t("URL Previews") }</h3>
|
<h3>{ _t("URL Previews") }</h3>
|
||||||
|
<div>
|
||||||
<label>{ previewsForAccount }</label>
|
{ _t('When someone puts a URL in their message, a URL preview can be shown to give more ' +
|
||||||
|
'information about that link such as the title, description, and an image from the website.') }
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{ previewsForAccount }
|
||||||
|
</div>
|
||||||
{ previewsForRoom }
|
{ previewsForRoom }
|
||||||
<label>{ previewsForRoomAccount }</label>
|
<label>{ previewsForRoomAccount }</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -27,7 +27,7 @@ import SdkConfig from '../../../SdkConfig';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import ScalarMessaging from '../../../ScalarMessaging';
|
import ScalarMessaging from '../../../ScalarMessaging';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import WidgetUtils from '../../../WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
// The maximum number of widgets that can be added in a room
|
// The maximum number of widgets that can be added in a room
|
||||||
|
@ -94,15 +94,7 @@ module.exports = React.createClass({
|
||||||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
case 'appsDrawer':
|
case 'appsDrawer':
|
||||||
// When opening the app drawer when there aren't any apps,
|
|
||||||
// auto-launch the integrations manager to skip the awkward
|
|
||||||
// click on "Add widget"
|
|
||||||
if (action.show) {
|
if (action.show) {
|
||||||
const apps = this._getApps();
|
|
||||||
if (apps.length === 0) {
|
|
||||||
this._launchManageIntegrations();
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.removeItem(hideWidgetKey);
|
localStorage.removeItem(hideWidgetKey);
|
||||||
} else {
|
} else {
|
||||||
// Store hidden state of widget
|
// Store hidden state of widget
|
||||||
|
@ -171,14 +163,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_getApps: function() {
|
_getApps: function() {
|
||||||
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets');
|
return WidgetUtils.getRoomWidgets(this.props.room).map((ev) => {
|
||||||
if (!appsStateEvents) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return appsStateEvents.filter((ev) => {
|
|
||||||
return ev.getContent().type && ev.getContent().url;
|
|
||||||
}).map((ev) => {
|
|
||||||
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,6 +34,7 @@ const ContextualMenu = require('../../structures/ContextualMenu');
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import {makeEventPermalink} from "../../../matrix-to";
|
import {makeEventPermalink} from "../../../matrix-to";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import {EventStatus} from 'matrix-js-sdk';
|
||||||
|
|
||||||
const ObjectUtils = require('../../../ObjectUtils');
|
const ObjectUtils = require('../../../ObjectUtils');
|
||||||
|
|
||||||
|
@ -442,26 +443,27 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
const ev = this.props.mxEvent;
|
const ev = this.props.mxEvent;
|
||||||
const props = {onClick: this.onCryptoClicked};
|
const props = {onClick: this.onCryptoClicked};
|
||||||
|
|
||||||
|
// event could not be decrypted
|
||||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||||
return <E2ePadlockUndecryptable {...props} />;
|
return <E2ePadlockUndecryptable {...props} />;
|
||||||
} else if (ev.isEncrypted()) {
|
}
|
||||||
if (this.state.verified) {
|
|
||||||
return <E2ePadlockVerified {...props} />;
|
|
||||||
} else {
|
|
||||||
return <E2ePadlockUnverified {...props} />;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// XXX: if the event is being encrypted (ie eventSendStatus ===
|
|
||||||
// encrypting), it might be nice to show something other than the
|
|
||||||
// open padlock?
|
|
||||||
|
|
||||||
// if the event is not encrypted, but it's an e2e room, show the
|
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||||
// open padlock
|
if (ev.isEncrypted()) {
|
||||||
const e2eEnabled = this.props.matrixClient.isRoomEncrypted(ev.getRoomId());
|
return this.state.verified ? <E2ePadlockVerified {...props} /> : <E2ePadlockUnverified {...props} />;
|
||||||
if (e2eEnabled) {
|
}
|
||||||
return <E2ePadlockUnencrypted {...props} />;
|
|
||||||
|
if (this.props.matrixClient.isRoomEncrypted(ev.getRoomId())) {
|
||||||
|
// else if room is encrypted
|
||||||
|
// and event is being encrypted or is not_sent (Unknown Devices/Network Error)
|
||||||
|
if (ev.status === EventStatus.ENCRYPTING) {
|
||||||
|
return <E2ePadlockEncrypting {...props} />;
|
||||||
}
|
}
|
||||||
|
if (ev.status === EventStatus.NOT_SENT) {
|
||||||
|
return <E2ePadlockNotSent {...props} />;
|
||||||
|
}
|
||||||
|
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
||||||
|
return <E2ePadlockUnencrypted {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no padlock needed
|
// no padlock needed
|
||||||
|
@ -490,7 +492,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||||
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
||||||
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
@ -608,13 +610,14 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
|
|
||||||
switch (this.props.tileShape) {
|
switch (this.props.tileShape) {
|
||||||
case 'notif': {
|
case 'notif': {
|
||||||
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<div className="mx_EventTile_roomName">
|
<div className="mx_EventTile_roomName">
|
||||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
|
||||||
{ room ? room.name : '' }
|
{ room ? room.name : '' }
|
||||||
</a>
|
</EmojiText>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_EventTile_senderDetails">
|
<div className="mx_EventTile_senderDetails">
|
||||||
{ avatar }
|
{ avatar }
|
||||||
|
@ -715,9 +718,15 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
|
||||||
|
const messageTypes = ['m.room.message', 'm.sticker'];
|
||||||
|
function isMessageEvent(ev) {
|
||||||
|
return (messageTypes.includes(ev.getType()));
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.haveTileForEvent = function(e) {
|
module.exports.haveTileForEvent = function(e) {
|
||||||
// Only messages have a tile (black-rectangle) if redacted
|
// Only messages have a tile (black-rectangle) if redacted
|
||||||
if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
|
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||||
|
|
||||||
const handler = getHandlerTile(e);
|
const handler = getHandlerTile(e);
|
||||||
if (handler === undefined) return false;
|
if (handler === undefined) return false;
|
||||||
|
@ -736,6 +745,14 @@ function E2ePadlockUndecryptable(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function E2ePadlockEncrypting(props) {
|
||||||
|
return <E2ePadlock alt={_t("Encrypting")} src="img/e2e-encrypting.svg" width="10" height="12" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function E2ePadlockNotSent(props) {
|
||||||
|
return <E2ePadlock alt={_t("Encrypted, not sent")} src="img/e2e-not_sent.svg" width="10" height="12" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
function E2ePadlockVerified(props) {
|
function E2ePadlockVerified(props) {
|
||||||
return (
|
return (
|
||||||
<E2ePadlock alt={_t("Encrypted by a verified device")}
|
<E2ePadlock alt={_t("Encrypted by a verified device")}
|
||||||
|
|
|
@ -652,6 +652,13 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onShareUserClick: function() {
|
||||||
|
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||||
|
Modal.createTrackedDialog('share room member dialog', '', ShareDialog, {
|
||||||
|
target: this.props.member,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_renderUserOptions: function() {
|
_renderUserOptions: function() {
|
||||||
const cli = this.props.matrixClient;
|
const cli = this.props.matrixClient;
|
||||||
const member = this.props.member;
|
const member = this.props.member;
|
||||||
|
@ -725,13 +732,18 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ignoreButton && !readReceiptButton && !insertPillButton && !inviteUserButton) return null;
|
const shareUserButton = (
|
||||||
|
<AccessibleButton onClick={this.onShareUserClick} className="mx_MemberInfo_field">
|
||||||
|
{ _t('Share Link to User') }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>{ _t("User Options") }</h3>
|
<h3>{ _t("User Options") }</h3>
|
||||||
<div className="mx_MemberInfo_buttons">
|
<div className="mx_MemberInfo_buttons">
|
||||||
{ readReceiptButton }
|
{ readReceiptButton }
|
||||||
|
{ shareUserButton }
|
||||||
{ insertPillButton }
|
{ insertPillButton }
|
||||||
{ ignoreButton }
|
{ ignoreButton }
|
||||||
{ inviteUserButton }
|
{ inviteUserButton }
|
||||||
|
@ -922,7 +934,9 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberInfo">
|
<div className="mx_MemberInfo">
|
||||||
<GeminiScrollbarWrapper autoshow={true}>
|
<GeminiScrollbarWrapper autoshow={true}>
|
||||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
|
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
|
||||||
|
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||||
|
</AccessibleButton>
|
||||||
<div className="mx_MemberInfo_avatar">
|
<div className="mx_MemberInfo_avatar">
|
||||||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -159,54 +159,20 @@ export default class MessageComposer extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// _startCallApp(isAudioConf) {
|
|
||||||
// dis.dispatch({
|
|
||||||
// action: 'appsDrawer',
|
|
||||||
// show: true,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
|
||||||
// let appsStateEvent = {};
|
|
||||||
// if (appsStateEvents) {
|
|
||||||
// appsStateEvent = appsStateEvents.getContent();
|
|
||||||
// }
|
|
||||||
// if (!appsStateEvent.videoConf) {
|
|
||||||
// appsStateEvent.videoConf = {
|
|
||||||
// type: 'jitsi',
|
|
||||||
// // FIXME -- This should not be localhost
|
|
||||||
// url: 'http://localhost:8000/jitsi.html',
|
|
||||||
// data: {
|
|
||||||
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
|
|
||||||
// isAudioConf: isAudioConf,
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// MatrixClientPeg.get().sendStateEvent(
|
|
||||||
// this.props.room.roomId,
|
|
||||||
// 'im.vector.modular.widgets',
|
|
||||||
// appsStateEvent,
|
|
||||||
// '',
|
|
||||||
// ).then(() => console.log('Sent state'), (e) => console.error(e));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
onCallClick(ev) {
|
onCallClick(ev) {
|
||||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
type: ev.shiftKey ? "screensharing" : "video",
|
type: ev.shiftKey ? "screensharing" : "video",
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
});
|
});
|
||||||
// this._startCallApp(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onVoiceCallClick(ev) {
|
onVoiceCallClick(ev) {
|
||||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
type: "voice",
|
type: "voice",
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
});
|
});
|
||||||
// this._startCallApp(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||||
|
|
|
@ -28,7 +28,7 @@ import Promise from 'bluebird';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||||
import SlashCommands from '../../../SlashCommands';
|
import {processCommandInput} from '../../../SlashCommands';
|
||||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
@ -45,8 +45,7 @@ import Markdown from '../../../Markdown';
|
||||||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
||||||
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
||||||
|
|
||||||
import {MATRIXTO_URL_PATTERN, MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix';
|
import {MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix';
|
||||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
|
||||||
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
|
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
|
||||||
|
|
||||||
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
|
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
|
||||||
|
@ -158,6 +157,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
||||||
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
||||||
this.onTextPasted = this.onTextPasted.bind(this);
|
this.onTextPasted = this.onTextPasted.bind(this);
|
||||||
|
this.focusComposer = this.focusComposer.bind(this);
|
||||||
|
|
||||||
const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
|
const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
|
||||||
|
|
||||||
|
@ -271,13 +271,12 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = (payload) => {
|
onAction = (payload) => {
|
||||||
const editor = this.refs.editor;
|
|
||||||
let contentState = this.state.editorState.getCurrentContent();
|
let contentState = this.state.editorState.getCurrentContent();
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'reply_to_event':
|
case 'reply_to_event':
|
||||||
case 'focus_composer':
|
case 'focus_composer':
|
||||||
editor.focus();
|
this.focusComposer();
|
||||||
break;
|
break;
|
||||||
case 'insert_mention': {
|
case 'insert_mention': {
|
||||||
// Pretend that we've autocompleted this user because keeping two code
|
// Pretend that we've autocompleted this user because keeping two code
|
||||||
|
@ -320,7 +319,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||||
editorState = EditorState.moveSelectionToEnd(editorState);
|
editorState = EditorState.moveSelectionToEnd(editorState);
|
||||||
this.onEditorContentChanged(editorState);
|
this.onEditorContentChanged(editorState);
|
||||||
editor.focus();
|
this.focusComposer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -722,7 +721,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
// Some commands (/join) require pills to be replaced with their text content
|
// Some commands (/join) require pills to be replaced with their text content
|
||||||
const commandText = this.removeMDLinks(contentState, ['#']);
|
const commandText = this.removeMDLinks(contentState, ['#']);
|
||||||
const cmd = SlashCommands.processInput(this.props.room.roomId, commandText);
|
const cmd = processCommandInput(this.props.room.roomId, commandText);
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (!cmd.error) {
|
if (!cmd.error) {
|
||||||
this.historyManager.save(contentState, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
this.historyManager.save(contentState, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
||||||
|
@ -1156,6 +1155,10 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.handleKeyCommand('toggle-mode');
|
this.handleKeyCommand('toggle-mode');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
focusComposer() {
|
||||||
|
this.refs.editor.focus();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||||
|
|
||||||
|
@ -1180,9 +1183,9 @@ export default class MessageComposerInput extends React.Component {
|
||||||
activeEditorState.getCurrentContent().getBlocksAsArray());
|
activeEditorState.getCurrentContent().getBlocksAsArray());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageComposer_input_wrapper">
|
<div className="mx_MessageComposer_input_wrapper" onClick={this.focusComposer}>
|
||||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||||
{ SettingsStore.isFeatureEnabled("feature_rich_quoting") && <ReplyPreview /> }
|
<ReplyPreview />
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
ref={(e) => this.autocomplete = e}
|
ref={(e) => this.autocomplete = e}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
|
|
|
@ -149,6 +149,13 @@ module.exports = React.createClass({
|
||||||
dis.dispatch({ action: 'show_right_panel' });
|
dis.dispatch({ action: 'show_right_panel' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onShareRoomClick: function(ev) {
|
||||||
|
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||||
|
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
|
||||||
|
target: this.props.room,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_hasUnreadPins: function() {
|
_hasUnreadPins: function() {
|
||||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||||
if (!currentPinEvent) return false;
|
if (!currentPinEvent) return false;
|
||||||
|
@ -379,6 +386,14 @@ module.exports = React.createClass({
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shareRoomButton;
|
||||||
|
if (this.props.inRoom) {
|
||||||
|
shareRoomButton =
|
||||||
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShareRoomClick} title={_t('Share room')}>
|
||||||
|
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
let rightPanelButtons;
|
let rightPanelButtons;
|
||||||
if (this.props.collapsedRhs) {
|
if (this.props.collapsedRhs) {
|
||||||
rightPanelButtons =
|
rightPanelButtons =
|
||||||
|
@ -400,6 +415,7 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomHeader_rightRow">
|
<div className="mx_RoomHeader_rightRow">
|
||||||
{ settingsButton }
|
{ settingsButton }
|
||||||
{ pinnedEventsButton }
|
{ pinnedEventsButton }
|
||||||
|
{ shareRoomButton }
|
||||||
{ manageIntegsButton }
|
{ manageIntegsButton }
|
||||||
{ forgetButton }
|
{ forgetButton }
|
||||||
{ searchButton }
|
{ searchButton }
|
||||||
|
|