Merge branches 'develop' and 't3chguy/edit_skip_if_no_edit' of github.com:matrix-org/matrix-react-sdk into t3chguy/edit_skip_if_no_edit
This commit is contained in:
commit
bf9353f3af
122 changed files with 3020 additions and 1327 deletions
|
@ -47,7 +47,6 @@ src/components/views/rooms/UserTile.js
|
||||||
src/components/views/settings/ChangeAvatar.js
|
src/components/views/settings/ChangeAvatar.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/Notifications.js
|
src/components/views/settings/Notifications.js
|
||||||
src/GroupAddressPicker.js
|
src/GroupAddressPicker.js
|
||||||
src/HtmlUtils.js
|
src/HtmlUtils.js
|
||||||
|
|
204
CHANGELOG.md
204
CHANGELOG.md
|
@ -1,3 +1,207 @@
|
||||||
|
Changes in [1.2.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.2) (2019-06-19)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.2-rc.2...v1.2.2)
|
||||||
|
|
||||||
|
No changes since rc.2
|
||||||
|
|
||||||
|
Changes in [1.2.2-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.2-rc.2) (2019-06-18)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.2-rc.1...v1.2.2-rc.2)
|
||||||
|
|
||||||
|
* Defer scalar API calls until they are needed
|
||||||
|
[\#3115](https://github.com/matrix-org/matrix-react-sdk/pull/3115)
|
||||||
|
* Blend pending redactions
|
||||||
|
[\#3117](https://github.com/matrix-org/matrix-react-sdk/pull/3117)
|
||||||
|
* Keep old arrow-up behaviour when editing is not enabled
|
||||||
|
[\#3116](https://github.com/matrix-org/matrix-react-sdk/pull/3116)
|
||||||
|
* Restore Composer History under shift-up & down
|
||||||
|
[\#3098](https://github.com/matrix-org/matrix-react-sdk/pull/3098)
|
||||||
|
* Allow changing server if validation has failed
|
||||||
|
[\#3114](https://github.com/matrix-org/matrix-react-sdk/pull/3114)
|
||||||
|
* Add Upload All button to UploadConfirmDialog
|
||||||
|
[\#3109](https://github.com/matrix-org/matrix-react-sdk/pull/3109)
|
||||||
|
* Re-enable register button
|
||||||
|
[\#3112](https://github.com/matrix-org/matrix-react-sdk/pull/3112)
|
||||||
|
* keep mx_Field stretching
|
||||||
|
[\#3111](https://github.com/matrix-org/matrix-react-sdk/pull/3111)
|
||||||
|
* Fix double-spinner
|
||||||
|
[\#3107](https://github.com/matrix-org/matrix-react-sdk/pull/3107)
|
||||||
|
* Fix display of canonicalAlias in group room info
|
||||||
|
[\#3110](https://github.com/matrix-org/matrix-react-sdk/pull/3110)
|
||||||
|
* Fix welcome user
|
||||||
|
[\#3106](https://github.com/matrix-org/matrix-react-sdk/pull/3106)
|
||||||
|
* Support editing emote messages
|
||||||
|
[\#3105](https://github.com/matrix-org/matrix-react-sdk/pull/3105)
|
||||||
|
* Use flex: 1 for mx_Field to replace all the calc(100% - 20px) and more
|
||||||
|
[\#3104](https://github.com/matrix-org/matrix-react-sdk/pull/3104)
|
||||||
|
* Use overflow on MemberInfo name/mxid so that the back button stays
|
||||||
|
[\#3099](https://github.com/matrix-org/matrix-react-sdk/pull/3099)
|
||||||
|
* Allow changing servers on nonfatal errors
|
||||||
|
[\#3102](https://github.com/matrix-org/matrix-react-sdk/pull/3102)
|
||||||
|
* Simplify email registration
|
||||||
|
[\#3101](https://github.com/matrix-org/matrix-react-sdk/pull/3101)
|
||||||
|
* Allow arrow keys navigation in autocomplete list
|
||||||
|
[\#2966](https://github.com/matrix-org/matrix-react-sdk/pull/2966)
|
||||||
|
* Edit unsent messages
|
||||||
|
[\#3097](https://github.com/matrix-org/matrix-react-sdk/pull/3097)
|
||||||
|
* Fix registration with email + non-default HS
|
||||||
|
[\#3096](https://github.com/matrix-org/matrix-react-sdk/pull/3096)
|
||||||
|
* Raise action bar above read marker
|
||||||
|
[\#3095](https://github.com/matrix-org/matrix-react-sdk/pull/3095)
|
||||||
|
* Console log more helpfully
|
||||||
|
[\#3094](https://github.com/matrix-org/matrix-react-sdk/pull/3094)
|
||||||
|
|
||||||
|
Changes in [1.2.2-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.2-rc.1) (2019-06-12)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.1...v1.2.2-rc.1)
|
||||||
|
|
||||||
|
* Align message context menu to right and vertically where space available
|
||||||
|
[\#3087](https://github.com/matrix-org/matrix-react-sdk/pull/3087)
|
||||||
|
* Allow registration to submit for non-fatal errors
|
||||||
|
[\#3093](https://github.com/matrix-org/matrix-react-sdk/pull/3093)
|
||||||
|
* Clear the login busy state after .well-known discovery
|
||||||
|
[\#3092](https://github.com/matrix-org/matrix-react-sdk/pull/3092)
|
||||||
|
* Update from Weblate
|
||||||
|
[\#3091](https://github.com/matrix-org/matrix-react-sdk/pull/3091)
|
||||||
|
* Fix registration after fail-fast
|
||||||
|
[\#3090](https://github.com/matrix-org/matrix-react-sdk/pull/3090)
|
||||||
|
* Use setBusy interface of js-sdk interactive auth
|
||||||
|
[\#3085](https://github.com/matrix-org/matrix-react-sdk/pull/3085)
|
||||||
|
* Don't handle identity server failure as fatal, and use the right message
|
||||||
|
[\#3088](https://github.com/matrix-org/matrix-react-sdk/pull/3088)
|
||||||
|
* Recheck message actions on decrypt
|
||||||
|
[\#3084](https://github.com/matrix-org/matrix-react-sdk/pull/3084)
|
||||||
|
* Fix exception on logout
|
||||||
|
[\#3086](https://github.com/matrix-org/matrix-react-sdk/pull/3086)
|
||||||
|
* Remember we were trying to accept an invite
|
||||||
|
[\#3083](https://github.com/matrix-org/matrix-react-sdk/pull/3083)
|
||||||
|
* Add funding details for GitHub sponsor button
|
||||||
|
[\#3079](https://github.com/matrix-org/matrix-react-sdk/pull/3079)
|
||||||
|
* Remove highlight from reactions
|
||||||
|
[\#3081](https://github.com/matrix-org/matrix-react-sdk/pull/3081)
|
||||||
|
* Clarify that only lowercase letters are allowed
|
||||||
|
[\#3080](https://github.com/matrix-org/matrix-react-sdk/pull/3080)
|
||||||
|
* Don't handle identity server liveliness errors as fatal
|
||||||
|
[\#3082](https://github.com/matrix-org/matrix-react-sdk/pull/3082)
|
||||||
|
* truncate long display names in timeline headings
|
||||||
|
[\#3078](https://github.com/matrix-org/matrix-react-sdk/pull/3078)
|
||||||
|
* Fail more softly on homeserver liveliness errors
|
||||||
|
[\#3067](https://github.com/matrix-org/matrix-react-sdk/pull/3067)
|
||||||
|
* Fix AddressPickerDialog adding wrong entry to selected list case
|
||||||
|
[\#3076](https://github.com/matrix-org/matrix-react-sdk/pull/3076)
|
||||||
|
* change profile keybind to backtick from i due to italics conflict
|
||||||
|
[\#3077](https://github.com/matrix-org/matrix-react-sdk/pull/3077)
|
||||||
|
* Look busy whilst requesting the email token
|
||||||
|
[\#3075](https://github.com/matrix-org/matrix-react-sdk/pull/3075)
|
||||||
|
* Fix email invites address-match checking
|
||||||
|
[\#3074](https://github.com/matrix-org/matrix-react-sdk/pull/3074)
|
||||||
|
* Add license info for Twemoji
|
||||||
|
[\#3073](https://github.com/matrix-org/matrix-react-sdk/pull/3073)
|
||||||
|
* Show read receipts on top of message
|
||||||
|
[\#3072](https://github.com/matrix-org/matrix-react-sdk/pull/3072)
|
||||||
|
* Be somewhat fuzzier when matching emojis to complete on space
|
||||||
|
[\#3070](https://github.com/matrix-org/matrix-react-sdk/pull/3070)
|
||||||
|
* Restrict reactions to a single emoji
|
||||||
|
[\#3069](https://github.com/matrix-org/matrix-react-sdk/pull/3069)
|
||||||
|
* Fix live updates to reaction row buttons
|
||||||
|
[\#3068](https://github.com/matrix-org/matrix-react-sdk/pull/3068)
|
||||||
|
* Don't refresh custom status on logout
|
||||||
|
[\#3065](https://github.com/matrix-org/matrix-react-sdk/pull/3065)
|
||||||
|
* Add a logged in class to EmbeddedPage and react to MatrixClient changes
|
||||||
|
[\#3066](https://github.com/matrix-org/matrix-react-sdk/pull/3066)
|
||||||
|
* Don't show "can't redact" dialog on network error, with redaction having
|
||||||
|
local echo & queuing now.
|
||||||
|
[\#3058](https://github.com/matrix-org/matrix-react-sdk/pull/3058)
|
||||||
|
* Fix login page breaking on wrong password
|
||||||
|
[\#3062](https://github.com/matrix-org/matrix-react-sdk/pull/3062)
|
||||||
|
* Update from Weblate
|
||||||
|
[\#3064](https://github.com/matrix-org/matrix-react-sdk/pull/3064)
|
||||||
|
* Install latest JS SDK when linting
|
||||||
|
[\#3063](https://github.com/matrix-org/matrix-react-sdk/pull/3063)
|
||||||
|
* Ensure we always show read receipts even with hidden events
|
||||||
|
[\#3056](https://github.com/matrix-org/matrix-react-sdk/pull/3056)
|
||||||
|
* Advance read receipts into trailing events without tiles
|
||||||
|
[\#3059](https://github.com/matrix-org/matrix-react-sdk/pull/3059)
|
||||||
|
* Remove unused errorText prop
|
||||||
|
[\#3061](https://github.com/matrix-org/matrix-react-sdk/pull/3061)
|
||||||
|
* Remove SettingsStore reference in RoomSettingsDialog
|
||||||
|
[\#3060](https://github.com/matrix-org/matrix-react-sdk/pull/3060)
|
||||||
|
* Custom notification sounds for rooms
|
||||||
|
[\#2928](https://github.com/matrix-org/matrix-react-sdk/pull/2928)
|
||||||
|
* Fix comments in unread room tracking
|
||||||
|
[\#3054](https://github.com/matrix-org/matrix-react-sdk/pull/3054)
|
||||||
|
* Allow source tile handler for replacements
|
||||||
|
[\#3057](https://github.com/matrix-org/matrix-react-sdk/pull/3057)
|
||||||
|
* Fix linting in MessagePanel
|
||||||
|
[\#3055](https://github.com/matrix-org/matrix-react-sdk/pull/3055)
|
||||||
|
* Convert breadcrumbs from labs to real setting
|
||||||
|
[\#3053](https://github.com/matrix-org/matrix-react-sdk/pull/3053)
|
||||||
|
* Add local echo on badges in breadcrumbs
|
||||||
|
[\#3052](https://github.com/matrix-org/matrix-react-sdk/pull/3052)
|
||||||
|
* Counteract smooth scrolling on breadcrumbs
|
||||||
|
[\#3051](https://github.com/matrix-org/matrix-react-sdk/pull/3051)
|
||||||
|
* add sbix fallback twemoji font (and bump to emoji 12)
|
||||||
|
[\#3050](https://github.com/matrix-org/matrix-react-sdk/pull/3050)
|
||||||
|
* Add option to change the default country code
|
||||||
|
[\#3049](https://github.com/matrix-org/matrix-react-sdk/pull/3049)
|
||||||
|
* Accept JSX into the GenericErrorPage and expose local session vars
|
||||||
|
[\#3043](https://github.com/matrix-org/matrix-react-sdk/pull/3043)
|
||||||
|
* Don't try and low encryption info when signing out in low bandwidth mode
|
||||||
|
[\#3048](https://github.com/matrix-org/matrix-react-sdk/pull/3048)
|
||||||
|
* only capture enter if something was selected in completions
|
||||||
|
[\#3047](https://github.com/matrix-org/matrix-react-sdk/pull/3047)
|
||||||
|
* Fix: better HTML > MD conversion for editing, including lists and quotes
|
||||||
|
[\#3040](https://github.com/matrix-org/matrix-react-sdk/pull/3040)
|
||||||
|
* Native emoji require extra line-height
|
||||||
|
[\#3044](https://github.com/matrix-org/matrix-react-sdk/pull/3044)
|
||||||
|
* port over low_bandwidth mode to develop
|
||||||
|
[\#2598](https://github.com/matrix-org/matrix-react-sdk/pull/2598)
|
||||||
|
* Fix: maintain caret at current line when position is on newline part
|
||||||
|
[\#3029](https://github.com/matrix-org/matrix-react-sdk/pull/3029)
|
||||||
|
* Remove username on HS input label
|
||||||
|
[\#3042](https://github.com/matrix-org/matrix-react-sdk/pull/3042)
|
||||||
|
* Exclude chrome in ua from safari version check for colr support
|
||||||
|
[\#3038](https://github.com/matrix-org/matrix-react-sdk/pull/3038)
|
||||||
|
* fix COLR font check being racy
|
||||||
|
[\#3034](https://github.com/matrix-org/matrix-react-sdk/pull/3034)
|
||||||
|
* Override font for usercontent download link
|
||||||
|
[\#3035](https://github.com/matrix-org/matrix-react-sdk/pull/3035)
|
||||||
|
* Revert "Make the timeline less noisy for screen readers (mk II) #3019"
|
||||||
|
[\#3033](https://github.com/matrix-org/matrix-react-sdk/pull/3033)
|
||||||
|
* Hide autocomplete on Enter key press instead of sending message
|
||||||
|
[\#2968](https://github.com/matrix-org/matrix-react-sdk/pull/2968)
|
||||||
|
* Message editing: arrow key (up/down) navigation between editable events
|
||||||
|
[\#3025](https://github.com/matrix-org/matrix-react-sdk/pull/3025)
|
||||||
|
* Message editing: fix reply text appearing in edit
|
||||||
|
[\#3032](https://github.com/matrix-org/matrix-react-sdk/pull/3032)
|
||||||
|
* Do not try to request thumbnails with non-integer widths
|
||||||
|
[\#3031](https://github.com/matrix-org/matrix-react-sdk/pull/3031)
|
||||||
|
* Message editing: preserve strikethrough as well
|
||||||
|
[\#3030](https://github.com/matrix-org/matrix-react-sdk/pull/3030)
|
||||||
|
* Add some logging for COLR checks
|
||||||
|
[\#3027](https://github.com/matrix-org/matrix-react-sdk/pull/3027)
|
||||||
|
* Fixup for tab completion: take part length into account as well
|
||||||
|
[\#3026](https://github.com/matrix-org/matrix-react-sdk/pull/3026)
|
||||||
|
* Message editing: tab completion
|
||||||
|
[\#3024](https://github.com/matrix-org/matrix-react-sdk/pull/3024)
|
||||||
|
* Message editing: dont jump to next part when inserting at *start* of
|
||||||
|
uneditable part
|
||||||
|
[\#3021](https://github.com/matrix-org/matrix-react-sdk/pull/3021)
|
||||||
|
* Message editing: preserve and re-apply formatting
|
||||||
|
[\#3013](https://github.com/matrix-org/matrix-react-sdk/pull/3013)
|
||||||
|
* Fix relationship between guests, .well-known, and auth
|
||||||
|
[\#3001](https://github.com/matrix-org/matrix-react-sdk/pull/3001)
|
||||||
|
* Restore use of full mxid login
|
||||||
|
[\#2972](https://github.com/matrix-org/matrix-react-sdk/pull/2972)
|
||||||
|
* Only expose the fallback_hs_url if the homeserver is the default homeserver
|
||||||
|
[\#2971](https://github.com/matrix-org/matrix-react-sdk/pull/2971)
|
||||||
|
* Refactor "Next" button into ServerConfig components
|
||||||
|
[\#2964](https://github.com/matrix-org/matrix-react-sdk/pull/2964)
|
||||||
|
* Render underlines and tooltips on custom server names in auth pages
|
||||||
|
[\#2965](https://github.com/matrix-org/matrix-react-sdk/pull/2965)
|
||||||
|
* Use validated server config for login, registration, and password reset
|
||||||
|
[\#2941](https://github.com/matrix-org/matrix-react-sdk/pull/2941)
|
||||||
|
|
||||||
Changes in [1.2.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.1) (2019-05-31)
|
Changes in [1.2.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.1) (2019-05-31)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.0...v1.2.1)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.0...v1.2.1)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -75,13 +75,13 @@
|
||||||
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279",
|
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279",
|
||||||
"gfm.css": "^1.1.1",
|
"gfm.css": "^1.1.1",
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.14",
|
||||||
"highlight.js": "9.14.2",
|
"highlight.js": "^9.15.8",
|
||||||
"is-ip": "^2.0.0",
|
"is-ip": "^2.0.0",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.6",
|
"linkifyjs": "^2.1.6",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"lolex": "2.3.2",
|
"lolex": "2.3.2",
|
||||||
"matrix-js-sdk": "2.0.0",
|
"matrix-js-sdk": "2.0.1",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"pako": "^1.0.5",
|
"pako": "^1.0.5",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
|
|
|
@ -50,7 +50,6 @@
|
||||||
@import "./views/context_menus/_TopLeftMenu.scss";
|
@import "./views/context_menus/_TopLeftMenu.scss";
|
||||||
@import "./views/dialogs/_AddressPickerDialog.scss";
|
@import "./views/dialogs/_AddressPickerDialog.scss";
|
||||||
@import "./views/dialogs/_Analytics.scss";
|
@import "./views/dialogs/_Analytics.scss";
|
||||||
@import "./views/dialogs/_BugReportDialog.scss";
|
|
||||||
@import "./views/dialogs/_ChangelogDialog.scss";
|
@import "./views/dialogs/_ChangelogDialog.scss";
|
||||||
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
|
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
|
||||||
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
||||||
|
@ -62,6 +61,7 @@
|
||||||
@import "./views/dialogs/_EncryptedEventDialog.scss";
|
@import "./views/dialogs/_EncryptedEventDialog.scss";
|
||||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||||
|
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||||
@import "./views/dialogs/_RestoreKeyBackupDialog.scss";
|
@import "./views/dialogs/_RestoreKeyBackupDialog.scss";
|
||||||
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
||||||
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
||||||
|
@ -87,6 +87,7 @@
|
||||||
@import "./views/elements/_Field.scss";
|
@import "./views/elements/_Field.scss";
|
||||||
@import "./views/elements/_ImageView.scss";
|
@import "./views/elements/_ImageView.scss";
|
||||||
@import "./views/elements/_InlineSpinner.scss";
|
@import "./views/elements/_InlineSpinner.scss";
|
||||||
|
@import "./views/elements/_InteractiveTooltip.scss";
|
||||||
@import "./views/elements/_ManageIntegsButton.scss";
|
@import "./views/elements/_ManageIntegsButton.scss";
|
||||||
@import "./views/elements/_MemberEventListSummary.scss";
|
@import "./views/elements/_MemberEventListSummary.scss";
|
||||||
@import "./views/elements/_MessageEditor.scss";
|
@import "./views/elements/_MessageEditor.scss";
|
||||||
|
@ -100,8 +101,8 @@
|
||||||
@import "./views/elements/_SyntaxHighlight.scss";
|
@import "./views/elements/_SyntaxHighlight.scss";
|
||||||
@import "./views/elements/_TextWithTooltip.scss";
|
@import "./views/elements/_TextWithTooltip.scss";
|
||||||
@import "./views/elements/_ToggleSwitch.scss";
|
@import "./views/elements/_ToggleSwitch.scss";
|
||||||
@import "./views/elements/_ToolTipButton.scss";
|
|
||||||
@import "./views/elements/_Tooltip.scss";
|
@import "./views/elements/_Tooltip.scss";
|
||||||
|
@import "./views/elements/_TooltipButton.scss";
|
||||||
@import "./views/elements/_Validation.scss";
|
@import "./views/elements/_Validation.scss";
|
||||||
@import "./views/globals/_MatrixToolbar.scss";
|
@import "./views/globals/_MatrixToolbar.scss";
|
||||||
@import "./views/groups/_GroupPublicityToggle.scss";
|
@import "./views/groups/_GroupPublicityToggle.scss";
|
||||||
|
@ -117,7 +118,8 @@
|
||||||
@import "./views/messages/_MTextBody.scss";
|
@import "./views/messages/_MTextBody.scss";
|
||||||
@import "./views/messages/_MessageActionBar.scss";
|
@import "./views/messages/_MessageActionBar.scss";
|
||||||
@import "./views/messages/_MessageTimestamp.scss";
|
@import "./views/messages/_MessageTimestamp.scss";
|
||||||
@import "./views/messages/_ReactionDimension.scss";
|
@import "./views/messages/_ReactionQuickTooltip.scss";
|
||||||
|
@import "./views/messages/_ReactionTooltipButton.scss";
|
||||||
@import "./views/messages/_ReactionsRow.scss";
|
@import "./views/messages/_ReactionsRow.scss";
|
||||||
@import "./views/messages/_ReactionsRowButton.scss";
|
@import "./views/messages/_ReactionsRowButton.scss";
|
||||||
@import "./views/messages/_ReactionsRowButtonTooltip.scss";
|
@import "./views/messages/_ReactionsRowButtonTooltip.scss";
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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.
|
||||||
|
@ -58,18 +59,6 @@ limitations under the License.
|
||||||
border-bottom: 8px solid transparent;
|
border-bottom: 8px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_chevron_right::after {
|
|
||||||
content: '';
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 7px solid transparent;
|
|
||||||
border-left: 7px solid $menu-bg-color;
|
|
||||||
border-bottom: 7px solid transparent;
|
|
||||||
position: absolute;
|
|
||||||
top: -7px;
|
|
||||||
right: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ContextualMenu_left {
|
.mx_ContextualMenu_left {
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
@ -89,18 +78,6 @@ limitations under the License.
|
||||||
border-bottom: 8px solid transparent;
|
border-bottom: 8px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_chevron_left::after {
|
|
||||||
content: '';
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 7px solid transparent;
|
|
||||||
border-right: 7px solid $menu-bg-color;
|
|
||||||
border-bottom: 7px solid transparent;
|
|
||||||
position: absolute;
|
|
||||||
top: -7px;
|
|
||||||
left: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ContextualMenu_top {
|
.mx_ContextualMenu_top {
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
@ -120,18 +97,6 @@ limitations under the License.
|
||||||
border-right: 8px solid transparent;
|
border-right: 8px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_chevron_top::after {
|
|
||||||
content: '';
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 7px solid transparent;
|
|
||||||
border-bottom: 7px solid $menu-bg-color;
|
|
||||||
border-right: 7px solid transparent;
|
|
||||||
position: absolute;
|
|
||||||
left: -7px;
|
|
||||||
top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ContextualMenu_bottom {
|
.mx_ContextualMenu_bottom {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
@ -151,24 +116,6 @@ limitations under the License.
|
||||||
border-right: 8px solid transparent;
|
border-right: 8px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_chevron_bottom::after {
|
|
||||||
content: '';
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 7px solid transparent;
|
|
||||||
border-top: 7px solid $menu-bg-color;
|
|
||||||
border-right: 7px solid transparent;
|
|
||||||
position: absolute;
|
|
||||||
left: -7px;
|
|
||||||
bottom: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ContextualMenu_field {
|
|
||||||
padding: 3px 6px 3px 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ContextualMenu_spinner {
|
.mx_ContextualMenu_spinner {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
@ -2,17 +2,15 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_GenericErrorPage_box {
|
.mx_GenericErrorPage_box {
|
||||||
position: absolute;
|
display: inline;
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: auto;
|
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 125px;
|
min-height: 125px;
|
||||||
border: 1px solid #f22;
|
border: 1px solid #f22;
|
||||||
padding: 10px 10px 20px;
|
padding: 10px 10px 20px;
|
||||||
background-color: #fcc;
|
background-color: #fcc;
|
||||||
|
|
|
@ -35,13 +35,6 @@ limitations under the License.
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomDirectory .gm-scroll-view {
|
|
||||||
// little hack because gemini doesn't seem to detect
|
|
||||||
// the scrollbar width well in this instance
|
|
||||||
// when using css scrollbars
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomDirectory_createRoom {
|
.mx_RoomDirectory_createRoom {
|
||||||
background-color: $button-bg-color;
|
background-color: $button-bg-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
|
@ -63,7 +63,6 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_Login_submit:disabled {
|
.mx_Login_submit:disabled {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody a.mx_Login_sso_link:link,
|
.mx_AuthBody a.mx_Login_sso_link:link,
|
||||||
|
|
|
@ -72,7 +72,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Field input {
|
.mx_Field input {
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +109,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_AuthBody_fieldRow > .mx_Field {
|
.mx_AuthBody_fieldRow > .mx_Field {
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody_fieldRow > .mx_Field:first-child {
|
.mx_AuthBody_fieldRow > .mx_Field:first-child {
|
||||||
|
|
|
@ -49,10 +49,14 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_InteractiveAuthEntryComponents_termsSubmit:disabled {
|
.mx_InteractiveAuthEntryComponents_termsSubmit:disabled {
|
||||||
background-color: $accent-color-50pct;
|
background-color: $accent-color-darker;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_InteractiveAuthEntryComponents_termsPolicy {
|
.mx_InteractiveAuthEntryComponents_termsPolicy {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveAuthEntryComponents_passwordSection {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ServerConfig_fields .mx_Field {
|
.mx_ServerConfig_fields .mx_Field {
|
||||||
flex: 1;
|
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,3 +21,7 @@ limitations under the License.
|
||||||
.mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section {
|
.mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section {
|
||||||
margin-top: 60px;
|
margin-top: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section .mx_Field {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
|
@ -23,7 +23,11 @@ limitations under the License.
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_ServersInRoomList_button, .mx_DevTools_RoomStateExplorer_query {
|
.mx_DevTools_RoomStateExplorer_query {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_ServersInRoomList_button {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -75,11 +79,6 @@ limitations under the License.
|
||||||
max-width: 684px;
|
max-width: 684px;
|
||||||
min-height: 250px;
|
min-height: 250px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_DevTools_content .mx_Field_input {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DevTools_eventTypeStateKeyGroup {
|
.mx_DevTools_eventTypeStateKeyGroup {
|
||||||
|
|
41
res/css/views/dialogs/_MessageEditHistoryDialog.scss
Normal file
41
res/css/views/dialogs/_MessageEditHistoryDialog.scss
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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_MessageEditHistoryDialog .mx_Dialog_header > .mx_Dialog_title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageEditHistoryDialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageEditHistoryDialog_scrollPanel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageEditHistoryDialog_edits {
|
||||||
|
list-style-type: none;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
|
||||||
|
.mx_EventTile_line, .mx_EventTile_content {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ limitations under the License.
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
width: 100%;
|
|
||||||
max-width: 280px;
|
max-width: 280px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,12 +42,6 @@ limitations under the License.
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EditableItemList_newItem .mx_Field input {
|
|
||||||
// Use 100% of the space available for the input, but don't let the 10px
|
|
||||||
// padding on either side of the input to push it out of alignment.
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EditableItemList_label {
|
.mx_EditableItemList_label {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_Field {
|
.mx_Field {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -42,6 +44,7 @@ limitations under the License.
|
||||||
padding: 8px 9px;
|
padding: 8px 9px;
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Field select {
|
.mx_Field select {
|
||||||
|
|
101
res/css/views/elements/_InteractiveTooltip.scss
Normal file
101
res/css/views/elements/_InteractiveTooltip.scss
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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_InteractiveTooltip_wrapper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip_background {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 1.0;
|
||||||
|
z-index: 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip {
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: $interactive-tooltip-bg-color;
|
||||||
|
color: $interactive-tooltip-fg-color;
|
||||||
|
position: absolute;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 6px;
|
||||||
|
z-index: 5001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top {
|
||||||
|
top: 10px; // 8px chevron + 2px spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip_chevron_top {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 8px);
|
||||||
|
top: -8px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
border-bottom: 8px solid $interactive-tooltip-bg-color;
|
||||||
|
border-right: 8px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path
|
||||||
|
// by Sebastiano Guerriero (@guerriero_se)
|
||||||
|
@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) {
|
||||||
|
.mx_InteractiveTooltip_chevron_top {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: inherit;
|
||||||
|
border: none;
|
||||||
|
clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
|
||||||
|
transform: rotate(135deg);
|
||||||
|
border-radius: 0 0 0 3px;
|
||||||
|
top: calc(-8px / 1.414); // sqrt(2) because of rotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom {
|
||||||
|
bottom: 10px; // 8px chevron + 2px spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip_chevron_bottom {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 8px);
|
||||||
|
bottom: -8px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
border-top: 8px solid $interactive-tooltip-bg-color;
|
||||||
|
border-right: 8px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path
|
||||||
|
// by Sebastiano Guerriero (@guerriero_se)
|
||||||
|
@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) {
|
||||||
|
.mx_InteractiveTooltip_chevron_bottom {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: inherit;
|
||||||
|
border: none;
|
||||||
|
clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
border-radius: 0 0 0 3px;
|
||||||
|
bottom: calc(-8px / 1.414); // sqrt(2) because of rotation
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,10 @@ limitations under the License.
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: $accent-color-50pct;
|
||||||
|
}
|
||||||
|
|
||||||
span.mx_UserPill, span.mx_RoomPill {
|
span.mx_UserPill, span.mx_RoomPill {
|
||||||
padding-left: 21px;
|
padding-left: 21px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -20,6 +20,5 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_PowerSelector .mx_Field select,
|
.mx_PowerSelector .mx_Field select,
|
||||||
.mx_PowerSelector .mx_Field input {
|
.mx_PowerSelector .mx_Field input {
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd.
|
Copyright 2017 New Vector Ltd.
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_ToolTipButton {
|
.mx_TooltipButton {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 11px;
|
width: 11px;
|
||||||
height: 11px;
|
height: 11px;
|
||||||
|
@ -33,17 +34,17 @@ limitations under the License.
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ToolTipButton:hover {
|
.mx_TooltipButton:hover {
|
||||||
opacity: 1.0;
|
opacity: 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ToolTipButton_container {
|
.mx_TooltipButton_container {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -18px;
|
top: -18px;
|
||||||
left: 4px;
|
left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ToolTipButton_helpText {
|
.mx_TooltipButton_helpText {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
line-height: 17px !important;
|
line-height: 17px !important;
|
|
@ -67,6 +67,10 @@ limitations under the License.
|
||||||
background-color: $message-action-bar-fg-color;
|
background-color: $message-action-bar-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar_reactButton::after {
|
||||||
|
mask-image: url('$(res)/img/react.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MessageActionBar_replyButton::after {
|
.mx_MessageActionBar_replyButton::after {
|
||||||
mask-image: url('$(res)/img/reply.svg');
|
mask-image: url('$(res)/img/reply.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,4 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_MessageTimestamp {
|
.mx_MessageTimestamp {
|
||||||
|
color: $event-timestamp-color;
|
||||||
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 OpenMarket Ltd
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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,12 +14,16 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_BugReportDialog .mx_Field {
|
.mx_ReactionsQuickTooltip_buttons {
|
||||||
flex: 1;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, auto);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BugReportDialog_field_input {
|
.mx_ReactionsQuickTooltip_label {
|
||||||
// TODO: We should really apply this to all .mx_Field inputs.
|
text-align: center;
|
||||||
// See https://github.com/vector-im/riot-web/issues/9344.
|
}
|
||||||
flex: 1;
|
|
||||||
|
.mx_ReactionsQuickTooltip_shortcode {
|
||||||
|
padding-left: 6px;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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,12 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_ReactionDimension {
|
.mx_ReactionTooltipButton {
|
||||||
width: 42px;
|
font-size: 16px;
|
||||||
display: flex;
|
padding: 6px;
|
||||||
justify-content: space-evenly;
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.25s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReactionDimension_disabled {
|
.mx_ReactionTooltipButton_selected {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
|
@ -18,3 +18,17 @@ limitations under the License.
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_ReactionsRow_showAll {
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 6px;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:link,
|
||||||
|
&:visited {
|
||||||
|
color: $accent-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -93,8 +93,6 @@ limitations under the License.
|
||||||
display: block;
|
display: block;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: $event-timestamp-color;
|
|
||||||
font-size: 10px;
|
|
||||||
left: 0px;
|
left: 0px;
|
||||||
width: 46px; /* 8 + 30 (avatar) + 8 */
|
width: 46px; /* 8 + 30 (avatar) + 8 */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -169,6 +167,9 @@ limitations under the License.
|
||||||
.mx_EventTile_sending .mx_RoomPill {
|
.mx_EventTile_sending .mx_RoomPill {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
.mx_EventTile_sending.mx_EventTile_redacted .mx_UnknownBody {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile_notSent {
|
.mx_EventTile_notSent {
|
||||||
color: $event-notsent-color;
|
color: $event-notsent-color;
|
||||||
|
@ -400,6 +401,7 @@ limitations under the License.
|
||||||
color: $roomtopic-color;
|
color: $roomtopic-color;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 9px;
|
margin-left: 9px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Various markdown overrides */
|
/* Various markdown overrides */
|
||||||
|
|
|
@ -43,6 +43,8 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_MemberInfo_name h2 {
|
.mx_MemberInfo_name h2 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MemberInfo h2 {
|
.mx_MemberInfo h2 {
|
||||||
|
|
|
@ -39,6 +39,16 @@ limitations under the License.
|
||||||
margin: 10px 10px 10px 0;
|
margin: 10px 10px 10px 0;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomPreviewBar_footer {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
.mx_Spinner {
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomPreviewBar_dark {
|
.mx_RoomPreviewBar_dark {
|
||||||
|
|
|
@ -15,17 +15,22 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_RoomUpgradeWarningBar {
|
.mx_RoomUpgradeWarningBar {
|
||||||
|
max-height: 235px;
|
||||||
|
background-color: $preview-bar-bg-color;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomUpgradeWarningBar_wrapped {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 235px;
|
|
||||||
background-color: $event-selected-color;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
display: flex;
|
|
||||||
background-color: $preview-bar-bg-color;
|
|
||||||
-webkit-align-items: center;
|
-webkit-align-items: center;
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomUpgradeWarningBar_header {
|
.mx_RoomUpgradeWarningBar_header {
|
||||||
|
|
|
@ -35,9 +35,3 @@ limitations under the License.
|
||||||
.mx_ExistingEmailAddress_confirmBtn {
|
.mx_ExistingEmailAddress_confirmBtn {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EmailAddresses_new .mx_Field input {
|
|
||||||
// Use 100% of the space available for the input, but don't let the 10px
|
|
||||||
// padding on either side of the input to push it out of alignment.
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
|
|
|
@ -29,3 +29,16 @@ limitations under the License.
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_IntegrationsManager_loading h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_IntegrationsManager_error {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_IntegrationsManager_error h3 {
|
||||||
|
color: $warning-color;
|
||||||
|
}
|
|
@ -36,12 +36,6 @@ limitations under the License.
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_PhoneNumbers_new .mx_Field input {
|
|
||||||
// Use 100% of the space available for the input, but don't let the 10px
|
|
||||||
// padding on either side of the input to push it out of alignment.
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PhoneNumbers_input {
|
.mx_PhoneNumbers_input {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -22,11 +22,6 @@ limitations under the License.
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ProfileSettings_controls .mx_Field #profileDisplayName,
|
|
||||||
.mx_ProfileSettings_controls .mx_Field #profileTopic {
|
|
||||||
width: calc(100% - 20px); // subtract 10px padding on left and right
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ProfileSettings_controls .mx_Field #profileTopic {
|
.mx_ProfileSettings_controls .mx_Field #profileTopic {
|
||||||
height: 4em;
|
height: 4em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,3 @@ limitations under the License.
|
||||||
.mx_GeneralRoomSettingsTab_profileSection {
|
.mx_GeneralRoomSettingsTab_profileSection {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_GeneralRoomSettingsTab .mx_AliasSettings .mx_Field select {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,33 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_GeneralUserSettingsTab_changePassword,
|
|
||||||
.mx_GeneralUserSettingsTab_themeSection {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_GeneralUserSettingsTab_changePassword .mx_Field,
|
.mx_GeneralUserSettingsTab_changePassword .mx_Field,
|
||||||
.mx_GeneralUserSettingsTab_themeSection .mx_Field {
|
.mx_GeneralUserSettingsTab_themeSection .mx_Field {
|
||||||
display: block;
|
|
||||||
margin-right: 100px; // Align with the other fields on the page
|
margin-right: 100px; // Align with the other fields on the page
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_GeneralUserSettingsTab_changePassword .mx_Field input {
|
|
||||||
display: block;
|
|
||||||
width: calc(100% - 20px); // subtract 10px padding on left and right
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_GeneralUserSettingsTab_changePassword .mx_Field:first-child {
|
.mx_GeneralUserSettingsTab_changePassword .mx_Field:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_GeneralUserSettingsTab_themeSection .mx_Field select {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses,
|
.mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses,
|
||||||
.mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers,
|
.mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers,
|
||||||
.mx_GeneralUserSettingsTab_languageInput {
|
.mx_GeneralUserSettingsTab_languageInput {
|
||||||
margin-right: 100px; // Align with the other fields on the page
|
margin-right: 100px; // Align with the other fields on the page
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,3 @@ limitations under the License.
|
||||||
.mx_PreferencesUserSettingsTab .mx_Field {
|
.mx_PreferencesUserSettingsTab .mx_Field {
|
||||||
margin-right: 100px; // Align with the rest of the controls
|
margin-right: 100px; // Align with the rest of the controls
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_PreferencesUserSettingsTab .mx_Field input {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
// Subtract 10px padding on left and right
|
|
||||||
// This is to keep the input aligned with the rest of the tab's controls.
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,11 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_VoiceUserSettingsTab .mx_Field select {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VoiceUserSettingsTab .mx_Field {
|
.mx_VoiceUserSettingsTab .mx_Field {
|
||||||
margin-right: 100px; // align with the rest of the fields
|
margin-right: 100px; // align with the rest of the fields
|
||||||
}
|
}
|
||||||
|
|
10
res/img/react.svg
Normal file
10
res/img/react.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="17" viewBox="0 0 18 17">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path fill="#2E2F32" fill-rule="nonzero" d="M13.6 14.5a7.875 7.875 0 1 1 2.183-3h-.818a7.125 7.125 0 1 0-2.62 3H13.6z"/>
|
||||||
|
<g stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75">
|
||||||
|
<path d="M11.5 13h6M14.5 10v6"/>
|
||||||
|
</g>
|
||||||
|
<path stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75" d="M5.5 10s1.125 1.5 3 1.5 3-1.5 3-1.5"/>
|
||||||
|
<path stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.25 6.25h.008M10.75 6.25h.008"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 696 B |
|
@ -160,6 +160,9 @@ $reaction-row-button-selected-border-color: $accent-color;
|
||||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||||
$tooltip-timeline-fg-color: #ffffff;
|
$tooltip-timeline-fg-color: #ffffff;
|
||||||
|
|
||||||
|
$interactive-tooltip-bg-color: $base-color;
|
||||||
|
$interactive-tooltip-fg-color: #ffffff;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -28,7 +28,8 @@ $focus-bg-color: #dddddd;
|
||||||
|
|
||||||
// button UI (white-on-green in light skin)
|
// button UI (white-on-green in light skin)
|
||||||
$accent-fg-color: #ffffff;
|
$accent-fg-color: #ffffff;
|
||||||
$accent-color-50pct: #92caad;
|
$accent-color-50pct: rgba(3, 179, 129, 0.5); //#03b381 in rgb
|
||||||
|
$accent-color-darker: #92caad;
|
||||||
$accent-color-alt: #238CF5;
|
$accent-color-alt: #238CF5;
|
||||||
|
|
||||||
$selection-fg-color: $primary-bg-color;
|
$selection-fg-color: $primary-bg-color;
|
||||||
|
@ -272,6 +273,9 @@ $reaction-row-button-selected-border-color: $accent-color;
|
||||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||||
$tooltip-timeline-fg-color: #ffffff;
|
$tooltip-timeline-fg-color: #ffffff;
|
||||||
|
|
||||||
|
$interactive-tooltip-bg-color: #27303a;
|
||||||
|
$interactive-tooltip-fg-color: #ffffff;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -344,7 +344,7 @@ function _onAction(payload) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _startCallApp(roomId, type) {
|
async function _startCallApp(roomId, type) {
|
||||||
// check for a working intgrations manager. Technically we could put
|
// check for a working integrations manager. Technically we could put
|
||||||
// the state event in anyway, but the resulting widget would then not
|
// the state event in anyway, but the resulting widget would then not
|
||||||
// work for us. Better that the user knows before everyone else in the
|
// work for us. Better that the user knows before everyone else in the
|
||||||
// room sees it.
|
// room sees it.
|
||||||
|
|
|
@ -18,6 +18,11 @@ import * as Matrix from 'matrix-js-sdk';
|
||||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
hasAnyLabeledDevices: async function() {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
return devices.some(d => !!d.label);
|
||||||
|
},
|
||||||
|
|
||||||
getDevices: function() {
|
getDevices: function() {
|
||||||
// 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
|
||||||
|
@ -26,8 +31,6 @@ export default {
|
||||||
const audioinput = [];
|
const audioinput = [];
|
||||||
const videoinput = [];
|
const videoinput = [];
|
||||||
|
|
||||||
if (devices.some((device) => !device.label)) return false;
|
|
||||||
|
|
||||||
devices.forEach((device) => {
|
devices.forEach((device) => {
|
||||||
switch (device.kind) {
|
switch (device.kind) {
|
||||||
case 'audiooutput': audiooutput.push(device); break;
|
case 'audiooutput': audiooutput.push(device); break;
|
||||||
|
|
86
src/ComposerHistoryManager.js
Normal file
86
src/ComposerHistoryManager.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
//@flow
|
||||||
|
/*
|
||||||
|
Copyright 2017 Aviral Dasgupta
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Value} from 'slate';
|
||||||
|
|
||||||
|
import _clamp from 'lodash/clamp';
|
||||||
|
|
||||||
|
type MessageFormat = 'rich' | 'markdown';
|
||||||
|
|
||||||
|
class HistoryItem {
|
||||||
|
// We store history items in their native format to ensure history is accurate
|
||||||
|
// and then convert them if our RTE has subsequently changed format.
|
||||||
|
value: Value;
|
||||||
|
format: MessageFormat = 'rich';
|
||||||
|
|
||||||
|
constructor(value: ?Value, format: ?MessageFormat) {
|
||||||
|
this.value = value;
|
||||||
|
this.format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJSON(obj: Object): HistoryItem {
|
||||||
|
return new HistoryItem(
|
||||||
|
Value.fromJSON(obj.value),
|
||||||
|
obj.format,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): Object {
|
||||||
|
return {
|
||||||
|
value: this.value.toJSON(),
|
||||||
|
format: this.format,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComposerHistoryManager {
|
||||||
|
history: Array<HistoryItem> = [];
|
||||||
|
prefix: string;
|
||||||
|
lastIndex: number = 0; // used for indexing the storage
|
||||||
|
currentIndex: number = 0; // used for indexing the loaded validated history Array
|
||||||
|
|
||||||
|
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
|
||||||
|
this.prefix = prefix + roomId;
|
||||||
|
|
||||||
|
// TODO: Performance issues?
|
||||||
|
let item;
|
||||||
|
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
||||||
|
try {
|
||||||
|
this.history.push(
|
||||||
|
HistoryItem.fromJSON(JSON.parse(item)),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Throwing away unserialisable history", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.lastIndex = this.currentIndex;
|
||||||
|
// reset currentIndex to account for any unserialisable history
|
||||||
|
this.currentIndex = this.history.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
save(value: Value, format: MessageFormat) {
|
||||||
|
const item = new HistoryItem(value, format);
|
||||||
|
this.history.push(item);
|
||||||
|
this.currentIndex = this.history.length;
|
||||||
|
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON()));
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(offset: number): ?HistoryItem {
|
||||||
|
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
|
||||||
|
return this.history[this.currentIndex];
|
||||||
|
}
|
||||||
|
}
|
|
@ -425,19 +425,25 @@ export default class ContentMessages {
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
|
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
|
||||||
|
let uploadAll = false;
|
||||||
for (let i = 0; i < okFiles.length; ++i) {
|
for (let i = 0; i < okFiles.length; ++i) {
|
||||||
const file = okFiles[i];
|
const file = okFiles[i];
|
||||||
const shouldContinue = await new Promise((resolve) => {
|
if (!uploadAll) {
|
||||||
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
|
const shouldContinue = await new Promise((resolve) => {
|
||||||
file,
|
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
|
||||||
currentIndex: i,
|
file,
|
||||||
totalFiles: okFiles.length,
|
currentIndex: i,
|
||||||
onFinished: (shouldContinue) => {
|
totalFiles: okFiles.length,
|
||||||
resolve(shouldContinue);
|
onFinished: (shouldContinue, shouldUploadAll) => {
|
||||||
},
|
if (shouldUploadAll) {
|
||||||
|
uploadAll = true;
|
||||||
|
}
|
||||||
|
resolve(shouldContinue);
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
if (!shouldContinue) break;
|
||||||
if (!shouldContinue) break;
|
}
|
||||||
this._sendContentToRoom(file, roomId, matrixClient);
|
this._sendContentToRoom(file, roomId, matrixClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,12 @@ limitations under the License.
|
||||||
|
|
||||||
import URL from 'url';
|
import URL from 'url';
|
||||||
import dis from './dispatcher';
|
import dis from './dispatcher';
|
||||||
import IntegrationManager from './IntegrationManager';
|
|
||||||
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
||||||
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
||||||
|
import sdk from "./index";
|
||||||
|
import Modal from "./Modal";
|
||||||
|
import MatrixClientPeg from "./MatrixClientPeg";
|
||||||
|
import RoomViewStore from "./stores/RoomViewStore";
|
||||||
|
|
||||||
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
||||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||||
|
@ -189,7 +192,14 @@ export default class FromWidgetPostMessageApi {
|
||||||
const data = event.data.data || event.data.widgetData;
|
const data = event.data.data || event.data.widgetData;
|
||||||
const integType = (data && data.integType) ? data.integType : null;
|
const integType = (data && data.integType) ? data.integType : null;
|
||||||
const integId = (data && data.integId) ? data.integId : null;
|
const integId = (data && data.integId) ? data.integId : null;
|
||||||
IntegrationManager.open(integType, integId);
|
|
||||||
|
// The dialog will take care of scalar auth for us
|
||||||
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
|
room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
||||||
|
screen: 'type_' + integType,
|
||||||
|
integrationId: integId,
|
||||||
|
}, "mx_IntegrationsManager");
|
||||||
} else if (action === 'set_always_on_screen') {
|
} else if (action === 'set_always_on_screen') {
|
||||||
// This is a new message: there is no reason to support the deprecated widgetData here
|
// This is a new message: there is no reason to support the deprecated widgetData here
|
||||||
const data = event.data.data;
|
const data = event.data.data;
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
import Modal from './Modal';
|
|
||||||
import sdk from './index';
|
|
||||||
import SdkConfig from './SdkConfig';
|
|
||||||
import ScalarMessaging from './ScalarMessaging';
|
|
||||||
import ScalarAuthClient from './ScalarAuthClient';
|
|
||||||
import RoomViewStore from './stores/RoomViewStore';
|
|
||||||
|
|
||||||
if (!global.mxIntegrationManager) {
|
|
||||||
global.mxIntegrationManager = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class IntegrationManager {
|
|
||||||
static _init() {
|
|
||||||
if (!global.mxIntegrationManager.client || !global.mxIntegrationManager.connected) {
|
|
||||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
|
||||||
ScalarMessaging.startListening();
|
|
||||||
global.mxIntegrationManager.client = new ScalarAuthClient();
|
|
||||||
|
|
||||||
return global.mxIntegrationManager.client.connect().then(() => {
|
|
||||||
global.mxIntegrationManager.connected = true;
|
|
||||||
}).catch((e) => {
|
|
||||||
console.error("Failed to connect to integrations server", e);
|
|
||||||
global.mxIntegrationManager.error = e;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('Invalid integration manager config', SdkConfig.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch the integrations manager on the stickers integration page
|
|
||||||
* @param {string} integName integration / widget type
|
|
||||||
* @param {string} integId integration / widget ID
|
|
||||||
* @param {function} onFinished Callback to invoke on integration manager close
|
|
||||||
*/
|
|
||||||
static async open(integName, integId, onFinished) {
|
|
||||||
await IntegrationManager._init();
|
|
||||||
if (global.mxIntegrationManager.client) {
|
|
||||||
await global.mxIntegrationManager.client.connect();
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
|
||||||
if (global.mxIntegrationManager.error ||
|
|
||||||
!(global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials())) {
|
|
||||||
console.error("Scalar error", global.mxIntegrationManager);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const integType = 'type_' + integName;
|
|
||||||
const src = (global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials()) ?
|
|
||||||
global.mxIntegrationManager.client.getScalarInterfaceUrlForRoom(
|
|
||||||
{roomId: RoomViewStore.getRoomId()},
|
|
||||||
integType,
|
|
||||||
integId,
|
|
||||||
) :
|
|
||||||
null;
|
|
||||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
|
||||||
src: src,
|
|
||||||
onFinished: onFinished,
|
|
||||||
}, "mx_IntegrationsManager");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -51,6 +51,7 @@ interface MatrixClientCreds {
|
||||||
class MatrixClientPeg {
|
class MatrixClientPeg {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.matrixClient = null;
|
this.matrixClient = null;
|
||||||
|
this._justRegisteredUserId = null;
|
||||||
|
|
||||||
// These are the default options used when when the
|
// These are the default options used when when the
|
||||||
// client is started in 'start'. These can be altered
|
// client is started in 'start'. These can be altered
|
||||||
|
@ -85,6 +86,31 @@ class MatrixClientPeg {
|
||||||
MatrixActionCreators.stop();
|
MatrixActionCreators.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If we've registered a user ID we set this to the ID of the
|
||||||
|
* user we've just registered. If they then go & log in, we
|
||||||
|
* can send them to the welcome user (obviously this doesn't
|
||||||
|
* guarentee they'll get a chat with the welcome user).
|
||||||
|
*
|
||||||
|
* @param {string} uid The user ID of the user we've just registered
|
||||||
|
*/
|
||||||
|
setJustRegisteredUserId(uid) {
|
||||||
|
this._justRegisteredUserId = uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns true if the current user has just been registered by this
|
||||||
|
* client as determined by setJustRegisteredUserId()
|
||||||
|
*
|
||||||
|
* @returns {bool} True if user has just been registered
|
||||||
|
*/
|
||||||
|
currentUserIsJustRegistered() {
|
||||||
|
return (
|
||||||
|
this.matrixClient &&
|
||||||
|
this.matrixClient.credentials.userId === this._justRegisteredUserId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace this MatrixClientPeg's client with a client instance that has
|
* Replace this MatrixClientPeg's client with a client instance that has
|
||||||
* homeserver / identity server URLs and active credentials
|
* homeserver / identity server URLs and active credentials
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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,13 +27,33 @@ export const MUTE = 'mute';
|
||||||
export const BADGE_STATES = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
export const BADGE_STATES = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
||||||
export const MENTION_BADGE_STATES = [...BADGE_STATES, MENTIONS_ONLY];
|
export const MENTION_BADGE_STATES = [...BADGE_STATES, MENTIONS_ONLY];
|
||||||
|
|
||||||
function _shouldShowNotifBadge(roomNotifState) {
|
export function shouldShowNotifBadge(roomNotifState) {
|
||||||
const showBadgeInStates = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
return BADGE_STATES.includes(roomNotifState);
|
||||||
return showBadgeInStates.indexOf(roomNotifState) > -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _shouldShowMentionBadge(roomNotifState) {
|
export function shouldShowMentionBadge(roomNotifState) {
|
||||||
return roomNotifState !== MUTE;
|
return MENTION_BADGE_STATES.includes(roomNotifState);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countRoomsWithNotif(rooms) {
|
||||||
|
return rooms.reduce((result, room, index) => {
|
||||||
|
const roomNotifState = getRoomNotifsState(room.roomId);
|
||||||
|
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||||
|
const notificationCount = room.getUnreadNotificationCount();
|
||||||
|
|
||||||
|
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
||||||
|
const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState);
|
||||||
|
const isInvite = room.hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite');
|
||||||
|
const badges = notifBadges || mentionBadges || isInvite;
|
||||||
|
|
||||||
|
if (badges) {
|
||||||
|
result.count++;
|
||||||
|
if (highlight) {
|
||||||
|
result.highlight = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, {count: 0, highlight: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aggregateNotificationCount(rooms) {
|
export function aggregateNotificationCount(rooms) {
|
||||||
|
@ -41,8 +62,8 @@ export function aggregateNotificationCount(rooms) {
|
||||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||||
const notificationCount = room.getUnreadNotificationCount();
|
const notificationCount = room.getUnreadNotificationCount();
|
||||||
|
|
||||||
const notifBadges = notificationCount > 0 && _shouldShowNotifBadge(roomNotifState);
|
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
||||||
const mentionBadges = highlight && _shouldShowMentionBadge(roomNotifState);
|
const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState);
|
||||||
const badges = notifBadges || mentionBadges;
|
const badges = notifBadges || mentionBadges;
|
||||||
|
|
||||||
if (badges) {
|
if (badges) {
|
||||||
|
@ -60,8 +81,8 @@ export function getRoomHasBadge(room) {
|
||||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||||
const notificationCount = room.getUnreadNotificationCount();
|
const notificationCount = room.getUnreadNotificationCount();
|
||||||
|
|
||||||
const notifBadges = notificationCount > 0 && _shouldShowNotifBadge(roomNotifState);
|
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
||||||
const mentionBadges = highlight && _shouldShowMentionBadge(roomNotifState);
|
const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState);
|
||||||
|
|
||||||
return notifBadges || mentionBadges;
|
return notifBadges || mentionBadges;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,14 @@ class ScalarAuthClient {
|
||||||
this.scalarToken = null;
|
this.scalarToken = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if setting up a ScalarAuthClient is even possible
|
||||||
|
* @returns {boolean} true if possible, false otherwise.
|
||||||
|
*/
|
||||||
|
static isPossible() {
|
||||||
|
return SdkConfig.get()['integrations_rest_url'] && SdkConfig.get()['integrations_ui_url'];
|
||||||
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
return this.getScalarToken().then((tok) => {
|
return this.getScalarToken().then((tok) => {
|
||||||
this.scalarToken = tok;
|
this.scalarToken = tok;
|
||||||
|
@ -41,7 +49,8 @@ class ScalarAuthClient {
|
||||||
|
|
||||||
// Returns a scalar_token string
|
// Returns a scalar_token string
|
||||||
getScalarToken() {
|
getScalarToken() {
|
||||||
const token = window.localStorage.getItem("mx_scalar_token");
|
let token = this.scalarToken;
|
||||||
|
if (!token) token = window.localStorage.getItem("mx_scalar_token");
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return this.registerForToken();
|
return this.registerForToken();
|
||||||
|
|
|
@ -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 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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,7 +16,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -48,7 +48,6 @@ export default class ContextualMenu extends React.Component {
|
||||||
menuWidth: PropTypes.number,
|
menuWidth: PropTypes.number,
|
||||||
menuHeight: PropTypes.number,
|
menuHeight: PropTypes.number,
|
||||||
chevronOffset: PropTypes.number,
|
chevronOffset: PropTypes.number,
|
||||||
menuColour: PropTypes.string,
|
|
||||||
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
||||||
// Function to be called on menu close
|
// Function to be called on menu close
|
||||||
onFinished: PropTypes.func,
|
onFinished: PropTypes.func,
|
||||||
|
@ -157,25 +156,6 @@ export default class ContextualMenu extends React.Component {
|
||||||
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
|
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
|
||||||
}
|
}
|
||||||
|
|
||||||
// To override the default chevron colour, if it's been set
|
|
||||||
let chevronCSS = "";
|
|
||||||
if (props.menuColour) {
|
|
||||||
chevronCSS = `
|
|
||||||
.mx_ContextualMenu_chevron_left:after {
|
|
||||||
border-right-color: ${props.menuColour};
|
|
||||||
}
|
|
||||||
.mx_ContextualMenu_chevron_right:after {
|
|
||||||
border-left-color: ${props.menuColour};
|
|
||||||
}
|
|
||||||
.mx_ContextualMenu_chevron_top:after {
|
|
||||||
border-left-color: ${props.menuColour};
|
|
||||||
}
|
|
||||||
.mx_ContextualMenu_chevron_bottom:after {
|
|
||||||
border-left-color: ${props.menuColour};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chevron = hasChevron ?
|
const chevron = hasChevron ?
|
||||||
<div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} /> :
|
<div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} /> :
|
||||||
undefined;
|
undefined;
|
||||||
|
@ -202,10 +182,6 @@ export default class ContextualMenu extends React.Component {
|
||||||
menuStyle.height = props.menuHeight;
|
menuStyle.height = props.menuHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.menuColour) {
|
|
||||||
menuStyle["backgroundColor"] = props.menuColour;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNaN(Number(props.menuPaddingTop))) {
|
if (!isNaN(Number(props.menuPaddingTop))) {
|
||||||
menuStyle["paddingTop"] = props.menuPaddingTop;
|
menuStyle["paddingTop"] = props.menuPaddingTop;
|
||||||
}
|
}
|
||||||
|
@ -236,7 +212,6 @@ export default class ContextualMenu extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
|
{ props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
|
||||||
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||||
<style>{ chevronCSS }</style>
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd.
|
Copyright 2017 Vector Creations Ltd.
|
||||||
Copyright 2017, 2018 New Vector Ltd.
|
Copyright 2017, 2018 New Vector Ltd.
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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.
|
||||||
|
@ -861,9 +862,9 @@ export default React.createClass({
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
|
const TooltipButton = sdk.getComponent('elements.TooltipButton');
|
||||||
|
|
||||||
const roomsHelpNode = this.state.editing ? <ToolTipButton helpText={
|
const roomsHelpNode = this.state.editing ? <TooltipButton helpText={
|
||||||
_t(
|
_t(
|
||||||
'These rooms are displayed to community members on the community page. '+
|
'These rooms are displayed to community members on the community page. '+
|
||||||
'Community members can join the rooms by clicking on them.',
|
'Community members can join the rooms by clicking on them.',
|
||||||
|
|
|
@ -38,6 +38,8 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
this.checkOverflow = this.checkOverflow.bind(this);
|
this.checkOverflow = this.checkOverflow.bind(this);
|
||||||
this._scrollElement = null;
|
this._scrollElement = null;
|
||||||
this._autoHideScrollbar = null;
|
this._autoHideScrollbar = null;
|
||||||
|
this._likelyTrackpadUser = null;
|
||||||
|
this._checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
leftIndicatorOffset: 0,
|
leftIndicatorOffset: 0,
|
||||||
|
@ -129,7 +131,27 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
|
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
|
||||||
const yRetention = 1.0;
|
const yRetention = 1.0;
|
||||||
|
|
||||||
if (Math.abs(e.deltaX) <= xyThreshold) {
|
// whenever we see horizontal scrolling, assume the user is on a trackpad
|
||||||
|
// for at least the next 1 minute.
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (Math.abs(e.deltaX) > 0) {
|
||||||
|
this._likelyTrackpadUser = true;
|
||||||
|
this._checkAgainForTrackpad = now + (1 * 60 * 1000);
|
||||||
|
} else {
|
||||||
|
// if we haven't seen any horizontal scrolling for a while, assume
|
||||||
|
// the user might have plugged in a mousewheel
|
||||||
|
if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) {
|
||||||
|
this._likelyTrackpadUser = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't mess with the horizontal scroll for trackpad users
|
||||||
|
// See https://github.com/vector-im/riot-web/issues/10005
|
||||||
|
if (this._likelyTrackpadUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(e.deltaX) <= xyThreshold) { // we are vertically scrolling.
|
||||||
// HACK: We increase the amount of scroll to counteract smooth scrolling browsers.
|
// HACK: We increase the amount of scroll to counteract smooth scrolling browsers.
|
||||||
// Smooth scrolling browsers (Firefox) use the relative area to determine the scroll
|
// Smooth scrolling browsers (Firefox) use the relative area to determine the scroll
|
||||||
// amount, which means the likely small area of content results in a small amount of
|
// amount, which means the likely small area of content results in a small amount of
|
||||||
|
|
|
@ -292,16 +292,6 @@ const LoggedInView = React.createClass({
|
||||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||||
|
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.UP:
|
|
||||||
case KeyCode.DOWN:
|
|
||||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
|
||||||
const action = ev.keyCode == KeyCode.UP ?
|
|
||||||
'view_prev_room' : 'view_next_room';
|
|
||||||
dis.dispatch({action: action});
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyCode.PAGE_UP:
|
case KeyCode.PAGE_UP:
|
||||||
case KeyCode.PAGE_DOWN:
|
case KeyCode.PAGE_DOWN:
|
||||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
|
|
|
@ -53,6 +53,8 @@ import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
||||||
|
import DMRoomMap from '../../utils/DMRoomMap';
|
||||||
|
import { countRoomsWithNotif } from '../../RoomNotifs';
|
||||||
|
|
||||||
// Disable warnings for now: we use deprecated bluebird functions
|
// Disable warnings for now: we use deprecated bluebird functions
|
||||||
// and need to migrate, but they spam the console with warnings.
|
// and need to migrate, but they spam the console with warnings.
|
||||||
|
@ -887,6 +889,7 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
MatrixClientPeg.setJustRegisteredUserId(credentials.user_id);
|
||||||
this.onRegistered(credentials);
|
this.onRegistered(credentials);
|
||||||
},
|
},
|
||||||
onDifferentServerClicked: (ev) => {
|
onDifferentServerClicked: (ev) => {
|
||||||
|
@ -1131,29 +1134,81 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a chat with the welcome user, if the user doesn't already have one
|
||||||
|
* @returns {string} The room ID of the new room, or null if no room was created
|
||||||
|
*/
|
||||||
|
async _startWelcomeUserChat() {
|
||||||
|
// We can end up with multiple tabs post-registration where the user
|
||||||
|
// might then end up with a session and we don't want them all making
|
||||||
|
// a chat with the welcome user: try to de-dupe.
|
||||||
|
// We need to wait for the first sync to complete for this to
|
||||||
|
// work though.
|
||||||
|
let waitFor;
|
||||||
|
if (!this.firstSyncComplete) {
|
||||||
|
waitFor = this.firstSyncPromise.promise;
|
||||||
|
} else {
|
||||||
|
waitFor = Promise.resolve();
|
||||||
|
}
|
||||||
|
await waitFor;
|
||||||
|
|
||||||
|
const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId(
|
||||||
|
this.props.config.welcomeUserId,
|
||||||
|
);
|
||||||
|
if (welcomeUserRooms.length === 0) {
|
||||||
|
const roomId = await createRoom({
|
||||||
|
dmUserId: this.props.config.welcomeUserId,
|
||||||
|
// Only view the welcome user if we're NOT looking at a room
|
||||||
|
andView: !this.state.currentRoomId,
|
||||||
|
spinner: false, // we're already showing one: we don't need another one
|
||||||
|
});
|
||||||
|
// This is a bit of a hack, but since the deduplication relies
|
||||||
|
// on m.direct being up to date, we need to force a sync
|
||||||
|
// of the database, otherwise if the user goes to the other
|
||||||
|
// tab before the next save happens (a few minutes), the
|
||||||
|
// saved sync will be restored from the db and this code will
|
||||||
|
// run without the update to m.direct, making another welcome
|
||||||
|
// user room (it doesn't wait for new data from the server, just
|
||||||
|
// the saved sync to be loaded).
|
||||||
|
const saveWelcomeUser = (ev) => {
|
||||||
|
if (
|
||||||
|
ev.getType() == 'm.direct' &&
|
||||||
|
ev.getContent() &&
|
||||||
|
ev.getContent()[this.props.config.welcomeUserId]
|
||||||
|
) {
|
||||||
|
MatrixClientPeg.get().store.save(true);
|
||||||
|
MatrixClientPeg.get().removeListener(
|
||||||
|
"accountData", saveWelcomeUser,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MatrixClientPeg.get().on("accountData", saveWelcomeUser);
|
||||||
|
|
||||||
|
return roomId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a new logged in session has started
|
* Called when a new logged in session has started
|
||||||
*/
|
*/
|
||||||
_onLoggedIn: async function() {
|
_onLoggedIn: async function() {
|
||||||
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
|
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
|
||||||
if (this._is_registered) {
|
if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
||||||
this._is_registered = false;
|
MatrixClientPeg.setJustRegisteredUserId(null);
|
||||||
|
|
||||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||||
const roomId = await createRoom({
|
const welcomeUserRoom = await this._startWelcomeUserChat();
|
||||||
dmUserId: this.props.config.welcomeUserId,
|
if (welcomeUserRoom === null) {
|
||||||
// Only view the welcome user if we're NOT looking at a room
|
// We didn't rediret to the welcome user room, so show
|
||||||
andView: !this.state.currentRoomId,
|
// the homepage.
|
||||||
});
|
dis.dispatch({action: 'view_home_page'});
|
||||||
// if successful, return because we're already
|
|
||||||
// viewing the welcomeUserId room
|
|
||||||
// else, if failed, fall through to view_home_page
|
|
||||||
if (roomId) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// The user has just logged in after registering,
|
||||||
|
// so show the homepage.
|
||||||
|
dis.dispatch({action: 'view_home_page'});
|
||||||
}
|
}
|
||||||
// The user has just logged in after registering
|
|
||||||
dis.dispatch({action: 'view_home_page'});
|
|
||||||
} else {
|
} else {
|
||||||
this._showScreenAfterLogin();
|
this._showScreenAfterLogin();
|
||||||
}
|
}
|
||||||
|
@ -1655,48 +1710,6 @@ export default React.createClass({
|
||||||
|
|
||||||
// returns a promise which resolves to the new MatrixClient
|
// returns a promise which resolves to the new MatrixClient
|
||||||
onRegistered: function(credentials) {
|
onRegistered: function(credentials) {
|
||||||
if (this.state.register_session_id) {
|
|
||||||
// The user came in through an email validation link. To avoid overwriting
|
|
||||||
// their session, check to make sure the session isn't someone else, and
|
|
||||||
// isn't a guest user since we'll usually have set a guest user session before
|
|
||||||
// starting the registration process. This isn't perfect since it's possible
|
|
||||||
// the user had a separate guest session they didn't actually mean to replace.
|
|
||||||
const sessionOwner = Lifecycle.getStoredSessionOwner();
|
|
||||||
const sessionIsGuest = Lifecycle.getStoredSessionIsGuest();
|
|
||||||
if (sessionOwner && !sessionIsGuest && sessionOwner !== credentials.userId) {
|
|
||||||
console.log(
|
|
||||||
`Found a session for ${sessionOwner} but ${credentials.userId} is trying to verify their ` +
|
|
||||||
`email address. Restoring the session for ${sessionOwner} with warning.`,
|
|
||||||
);
|
|
||||||
this._loadSession();
|
|
||||||
|
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
// N.B. first param is passed to piwik and so doesn't want i18n
|
|
||||||
Modal.createTrackedDialog('Existing session on register', '',
|
|
||||||
QuestionDialog, {
|
|
||||||
title: _t('You are logged in to another account'),
|
|
||||||
description: _t(
|
|
||||||
"Thank you for verifying your email! The account you're logged into here " +
|
|
||||||
"(%(sessionUserId)s) appears to be different from the account you've verified an " +
|
|
||||||
"email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, " +
|
|
||||||
"please log out first.", {
|
|
||||||
sessionUserId: sessionOwner,
|
|
||||||
verifiedUserId: credentials.userId,
|
|
||||||
|
|
||||||
// TODO: Fix translations to support reusing variables.
|
|
||||||
// https://github.com/vector-im/riot-web/issues/9086
|
|
||||||
verifiedUserId2: credentials.userId,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
hasCancelButton: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return MatrixClientPeg.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// XXX: This should be in state or ideally store(s) because we risk not
|
|
||||||
// rendering the most up-to-date view of state otherwise.
|
|
||||||
this._is_registered = true;
|
|
||||||
return Lifecycle.setLoggedIn(credentials);
|
return Lifecycle.setLoggedIn(credentials);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1737,19 +1750,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
updateStatusIndicator: function(state, prevState) {
|
updateStatusIndicator: function(state, prevState) {
|
||||||
let notifCount = 0;
|
const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getRooms()).count;
|
||||||
|
|
||||||
const rooms = MatrixClientPeg.get().getRooms();
|
|
||||||
for (let i = 0; i < rooms.length; ++i) {
|
|
||||||
if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) {
|
|
||||||
notifCount++;
|
|
||||||
} else if (rooms[i].getUnreadNotificationCount()) {
|
|
||||||
// if we were summing unread notifs:
|
|
||||||
// notifCount += rooms[i].getUnreadNotificationCount();
|
|
||||||
// instead, we just count the number of rooms with notifs.
|
|
||||||
notifCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PlatformPeg.get()) {
|
if (PlatformPeg.get()) {
|
||||||
PlatformPeg.get().setErrorStatus(state === 'ERROR');
|
PlatformPeg.get().setErrorStatus(state === 'ERROR');
|
||||||
|
|
|
@ -108,6 +108,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing");
|
||||||
// the event after which we put a visible unread marker on the last
|
// the event after which we put a visible unread marker on the last
|
||||||
// render cycle; null if readMarkerVisible was false or the RM was
|
// render cycle; null if readMarkerVisible was false or the RM was
|
||||||
// suppressed (eg because it was at the end of the timeline)
|
// suppressed (eg because it was at the end of the timeline)
|
||||||
|
@ -585,14 +586,14 @@ module.exports = React.createClass({
|
||||||
<EventTile mxEvent={mxEv}
|
<EventTile mxEvent={mxEv}
|
||||||
continuation={continuation}
|
continuation={continuation}
|
||||||
isRedacted={mxEv.isRedacted()}
|
isRedacted={mxEv.isRedacted()}
|
||||||
replacingEventId={mxEv.replacingEventId()}
|
replacingEventId={this._editingEnabled && mxEv.replacingEventId()}
|
||||||
editState={isEditing && this.props.editState}
|
editState={isEditing && this.props.editState}
|
||||||
onHeightChanged={this._onHeightChanged}
|
onHeightChanged={this._onHeightChanged}
|
||||||
readReceipts={readReceipts}
|
readReceipts={readReceipts}
|
||||||
readReceiptMap={this._readReceiptMap}
|
readReceiptMap={this._readReceiptMap}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
checkUnmounting={this._isUnmounting}
|
checkUnmounting={this._isUnmounting}
|
||||||
eventSendStatus={mxEv.replacementOrOwnStatus()}
|
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
isTwelveHour={this.props.isTwelveHour}
|
isTwelveHour={this.props.isTwelveHour}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
|
|
|
@ -333,7 +333,7 @@ module.exports = React.createClass({
|
||||||
if (alias.indexOf(':') == -1) {
|
if (alias.indexOf(':') == -1) {
|
||||||
alias = alias + ':' + this.state.roomServer;
|
alias = alias + ':' + this.state.roomServer;
|
||||||
}
|
}
|
||||||
this.showRoomAlias(alias);
|
this.showRoomAlias(alias, true);
|
||||||
} else {
|
} else {
|
||||||
// This is a 3rd party protocol. Let's see if we can join it
|
// This is a 3rd party protocol. Let's see if we can join it
|
||||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||||
|
@ -349,7 +349,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => {
|
MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => {
|
||||||
if (resp.length > 0 && resp[0].alias) {
|
if (resp.length > 0 && resp[0].alias) {
|
||||||
this.showRoomAlias(resp[0].alias);
|
this.showRoomAlias(resp[0].alias, true);
|
||||||
} else {
|
} else {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
||||||
|
@ -367,13 +367,16 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showRoomAlias: function(alias) {
|
showRoomAlias: function(alias, autoJoin=false) {
|
||||||
this.showRoom(null, alias);
|
this.showRoom(null, alias, autoJoin);
|
||||||
},
|
},
|
||||||
|
|
||||||
showRoom: function(room, room_alias) {
|
showRoom: function(room, room_alias, autoJoin=false) {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
const payload = {action: 'view_room'};
|
const payload = {
|
||||||
|
action: 'view_room',
|
||||||
|
auto_join: autoJoin,
|
||||||
|
};
|
||||||
if (room) {
|
if (room) {
|
||||||
// Don't let the user view a room they won't be able to either
|
// Don't let the user view a room they won't be able to either
|
||||||
// peek or join: fail earlier so they don't have to click back
|
// peek or join: fail earlier so they don't have to click back
|
||||||
|
|
|
@ -1523,9 +1523,11 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomView">
|
<div className="mx_RoomView">
|
||||||
<RoomPreviewBar
|
<RoomPreviewBar
|
||||||
canPreview={false}
|
canPreview={false}
|
||||||
|
previewLoading={this.state.peekLoading}
|
||||||
error={this.state.roomLoadError}
|
error={this.state.roomLoadError}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
joining={this.state.joining}
|
joining={this.state.joining}
|
||||||
|
oobData={this.props.oobData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -29,6 +29,7 @@ import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import * as Lifecycle from '../../../Lifecycle';
|
import * as Lifecycle from '../../../Lifecycle';
|
||||||
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
// Phases
|
// Phases
|
||||||
// Show controls to configure server details
|
// Show controls to configure server details
|
||||||
|
@ -96,6 +97,13 @@ module.exports = React.createClass({
|
||||||
// Our matrix client - part of state because we can't render the UI auth
|
// Our matrix client - part of state because we can't render the UI auth
|
||||||
// component without it.
|
// component without it.
|
||||||
matrixClient: null,
|
matrixClient: null,
|
||||||
|
|
||||||
|
// The user ID we've just registered
|
||||||
|
registeredUsername: null,
|
||||||
|
|
||||||
|
// if a different user ID to the one we just registered is logged in,
|
||||||
|
// this is the user ID that's logged in.
|
||||||
|
differentLoggedInUserId: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -167,6 +175,8 @@ module.exports = React.createClass({
|
||||||
_replaceClient: async function(serverConfig) {
|
_replaceClient: async function(serverConfig) {
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: null,
|
errorText: null,
|
||||||
|
serverDeadError: null,
|
||||||
|
serverErrorIsFatal: false,
|
||||||
// busy while we do liveness check (we need to avoid trying to render
|
// busy while we do liveness check (we need to avoid trying to render
|
||||||
// the UI auth component while we don't have a matrix client)
|
// the UI auth component while we don't have a matrix client)
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -179,7 +189,10 @@ module.exports = React.createClass({
|
||||||
serverConfig.hsUrl,
|
serverConfig.hsUrl,
|
||||||
serverConfig.isUrl,
|
serverConfig.isUrl,
|
||||||
);
|
);
|
||||||
this.setState({serverIsAlive: true});
|
this.setState({
|
||||||
|
serverIsAlive: true,
|
||||||
|
serverErrorIsFatal: false,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
|
@ -287,9 +300,29 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MatrixClientPeg.setJustRegisteredUserId(response.user_id);
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
doingUIAuth: false,
|
doingUIAuth: false,
|
||||||
|
registeredUsername: response.user_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The user came in through an email validation link. To avoid overwriting
|
||||||
|
// their session, check to make sure the session isn't someone else, and
|
||||||
|
// isn't a guest user since we'll usually have set a guest user session before
|
||||||
|
// starting the registration process. This isn't perfect since it's possible
|
||||||
|
// the user had a separate guest session they didn't actually mean to replace.
|
||||||
|
const sessionOwner = Lifecycle.getStoredSessionOwner();
|
||||||
|
const sessionIsGuest = Lifecycle.getStoredSessionIsGuest();
|
||||||
|
if (sessionOwner && !sessionIsGuest && sessionOwner !== response.userId) {
|
||||||
|
console.log(
|
||||||
|
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
|
||||||
|
);
|
||||||
|
newState.differentLoggedInUserId = sessionOwner;
|
||||||
|
} else {
|
||||||
|
newState.differentLoggedInUserId = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.access_token) {
|
if (response.access_token) {
|
||||||
const cli = await this.props.onLoggedIn({
|
const cli = await this.props.onLoggedIn({
|
||||||
userId: response.user_id,
|
userId: response.user_id,
|
||||||
|
@ -423,7 +456,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// If we're on a different phase, we only show the server type selector,
|
// If we're on a different phase, we only show the server type selector,
|
||||||
// which is always shown if we allow custom URLs at all.
|
// which is always shown if we allow custom URLs at all.
|
||||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
|
// (if there's a fatal server error, we need to show the full server
|
||||||
|
// config as the user may need to change servers to resolve the error).
|
||||||
|
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) {
|
||||||
return <div>
|
return <div>
|
||||||
<ServerTypeSelector
|
<ServerTypeSelector
|
||||||
selected={this.state.serverType}
|
selected={this.state.serverType}
|
||||||
|
@ -528,6 +563,7 @@ module.exports = React.createClass({
|
||||||
const AuthHeader = sdk.getComponent('auth.AuthHeader');
|
const AuthHeader = sdk.getComponent('auth.AuthHeader');
|
||||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||||
const AuthPage = sdk.getComponent('auth.AuthPage');
|
const AuthPage = sdk.getComponent('auth.AuthPage');
|
||||||
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
|
||||||
let errorText;
|
let errorText;
|
||||||
const err = this.state.errorText;
|
const err = this.state.errorText;
|
||||||
|
@ -564,28 +600,41 @@ module.exports = React.createClass({
|
||||||
let body;
|
let body;
|
||||||
if (this.state.completedNoSignin) {
|
if (this.state.completedNoSignin) {
|
||||||
let regDoneText;
|
let regDoneText;
|
||||||
if (this.state.formVals.password) {
|
if (this.state.differentLoggedInUserId) {
|
||||||
|
regDoneText = <div>
|
||||||
|
<p>{_t(
|
||||||
|
"Your new account (%(newAccountId)s) is registered, but you're already " +
|
||||||
|
"logged into a different account (%(loggedInUserId)s).", {
|
||||||
|
newAccountId: this.state.registeredUsername,
|
||||||
|
loggedInUserId: this.state.differentLoggedInUserId,
|
||||||
|
},
|
||||||
|
)}</p>
|
||||||
|
<p><AccessibleButton element="span" className="mx_linkButton" onClick={this._onLoginClickWithCheck}>
|
||||||
|
{_t("Continue with previous account")}
|
||||||
|
</AccessibleButton></p>
|
||||||
|
</div>;
|
||||||
|
} else if (this.state.formVals.password) {
|
||||||
// We're the client that started the registration
|
// We're the client that started the registration
|
||||||
regDoneText = _t(
|
regDoneText = <h3>{_t(
|
||||||
"<a>Log in</a> to your new account.", {},
|
"<a>Log in</a> to your new account.", {},
|
||||||
{
|
{
|
||||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
||||||
},
|
},
|
||||||
);
|
)}</h3>;
|
||||||
} else {
|
} else {
|
||||||
// We're not the original client: the user probably got to us by clicking the
|
// We're not the original client: the user probably got to us by clicking the
|
||||||
// email validation link. We can't offer a 'go straight to your account' link
|
// email validation link. We can't offer a 'go straight to your account' link
|
||||||
// as we don't have the original creds.
|
// as we don't have the original creds.
|
||||||
regDoneText = _t(
|
regDoneText = <h3>{_t(
|
||||||
"You can now close this window or <a>log in</a> to your new account.", {},
|
"You can now close this window or <a>log in</a> to your new account.", {},
|
||||||
{
|
{
|
||||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
||||||
},
|
},
|
||||||
);
|
)}</h3>;
|
||||||
}
|
}
|
||||||
body = <div>
|
body = <div>
|
||||||
<h2>{_t("Registration Successful")}</h2>
|
<h2>{_t("Registration Successful")}</h2>
|
||||||
<h3>{ regDoneText }</h3>
|
{ regDoneText }
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
body = <div>
|
body = <div>
|
||||||
|
|
|
@ -81,16 +81,10 @@ export const PasswordAuthEntry = React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
passwordValid: false,
|
password: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
focus: function() {
|
|
||||||
if (this.refs.passwordField) {
|
|
||||||
this.refs.passwordField.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_onSubmit: function(e) {
|
_onSubmit: function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.props.busy) return;
|
if (this.props.busy) return;
|
||||||
|
@ -98,23 +92,21 @@ export const PasswordAuthEntry = React.createClass({
|
||||||
this.props.submitAuthDict({
|
this.props.submitAuthDict({
|
||||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
type: PasswordAuthEntry.LOGIN_TYPE,
|
||||||
user: this.props.matrixClient.credentials.userId,
|
user: this.props.matrixClient.credentials.userId,
|
||||||
password: this.refs.passwordField.value,
|
password: this.state.password,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onPasswordFieldChange: function(ev) {
|
_onPasswordFieldChange: function(ev) {
|
||||||
// enable the submit button iff the password is non-empty
|
// enable the submit button iff the password is non-empty
|
||||||
this.setState({
|
this.setState({
|
||||||
passwordValid: Boolean(this.refs.passwordField.value),
|
password: ev.target.value,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let passwordBoxClass = null;
|
const passwordBoxClass = classnames({
|
||||||
|
"error": this.props.errorText,
|
||||||
if (this.props.errorText) {
|
});
|
||||||
passwordBoxClass = 'error';
|
|
||||||
}
|
|
||||||
|
|
||||||
let submitButtonOrSpinner;
|
let submitButtonOrSpinner;
|
||||||
if (this.props.busy) {
|
if (this.props.busy) {
|
||||||
|
@ -124,7 +116,7 @@ export const PasswordAuthEntry = React.createClass({
|
||||||
submitButtonOrSpinner = (
|
submitButtonOrSpinner = (
|
||||||
<input type="submit"
|
<input type="submit"
|
||||||
className="mx_Dialog_primary"
|
className="mx_Dialog_primary"
|
||||||
disabled={!this.state.passwordValid}
|
disabled={!this.state.password}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -138,17 +130,21 @@ export const PasswordAuthEntry = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Field = sdk.getComponent('elements.Field');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{ _t("To continue, please enter your password.") }</p>
|
<p>{ _t("To continue, please enter your password.") }</p>
|
||||||
<form onSubmit={this._onSubmit}>
|
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||||
<label htmlFor="passwordField">{ _t("Password:") }</label>
|
<Field
|
||||||
<input
|
id="mx_InteractiveAuthEntryComponents_password"
|
||||||
name="passwordField"
|
|
||||||
ref="passwordField"
|
|
||||||
className={passwordBoxClass}
|
className={passwordBoxClass}
|
||||||
onChange={this._onPasswordFieldChange}
|
|
||||||
type="password"
|
type="password"
|
||||||
|
name="passwordField"
|
||||||
|
label={_t('Password')}
|
||||||
|
autoFocus={true}
|
||||||
|
value={this.state.password}
|
||||||
|
onChange={this._onPasswordFieldChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_button_row">
|
<div className="mx_button_row">
|
||||||
{ submitButtonOrSpinner }
|
{ submitButtonOrSpinner }
|
||||||
|
|
|
@ -104,6 +104,9 @@ export default class ServerConfig extends React.PureComponent {
|
||||||
|
|
||||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||||
if (!stateForError.isFatalError) {
|
if (!stateForError.isFatalError) {
|
||||||
|
this.setState({
|
||||||
|
busy: false,
|
||||||
|
});
|
||||||
// carry on anyway
|
// carry on anyway
|
||||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
|
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
|
||||||
this.props.onServerConfigChange(result);
|
this.props.onServerConfigChange(result);
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
confirmButtonEnabled: false,
|
password: "",
|
||||||
busy: false,
|
busy: false,
|
||||||
shouldErase: false,
|
shouldErase: false,
|
||||||
errStr: null,
|
errStr: null,
|
||||||
|
@ -45,7 +45,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
|
|
||||||
_onPasswordFieldChange(ev) {
|
_onPasswordFieldChange(ev) {
|
||||||
this.setState({
|
this.setState({
|
||||||
confirmButtonEnabled: Boolean(ev.target.value),
|
password: ev.target.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
|
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
|
||||||
const okEnabled = this.state.confirmButtonEnabled && !this.state.busy;
|
const okEnabled = this.state.password && !this.state.busy;
|
||||||
|
|
||||||
let cancelButton = null;
|
let cancelButton = null;
|
||||||
if (!this.state.busy) {
|
if (!this.state.busy) {
|
||||||
|
@ -113,6 +113,8 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Field = sdk.getComponent('elements.Field');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_DeactivateAccountDialog"
|
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
|
@ -167,10 +169,12 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>{ _t("To continue, please enter your password:") }</p>
|
<p>{ _t("To continue, please enter your password:") }</p>
|
||||||
<input
|
<Field
|
||||||
|
id="mx_DeactivateAccountDialog_password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={_t("password")}
|
label={_t('Password')}
|
||||||
onChange={this._onPasswordFieldChange}
|
onChange={this._onPasswordFieldChange}
|
||||||
|
value={this.state.password}
|
||||||
ref={(e) => {this._passwordField = e;}}
|
ref={(e) => {this._passwordField = e;}}
|
||||||
className={passwordBoxClass}
|
className={passwordBoxClass}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -34,9 +34,15 @@ export default class IncomingSasDialog extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
let phase = PHASE_START;
|
||||||
|
if (this.props.verifier.cancelled) {
|
||||||
|
console.log("Verifier was cancelled in the background.");
|
||||||
|
phase = PHASE_CANCELLED;
|
||||||
|
}
|
||||||
|
|
||||||
this._showSasEvent = null;
|
this._showSasEvent = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: PHASE_START,
|
phase: phase,
|
||||||
sasVerified: false,
|
sasVerified: false,
|
||||||
opponentProfile: null,
|
opponentProfile: null,
|
||||||
opponentProfileError: null,
|
opponentProfileError: null,
|
||||||
|
|
108
src/components/views/dialogs/MessageEditHistoryDialog.js
Normal file
108
src/components/views/dialogs/MessageEditHistoryDialog.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import sdk from "../../../index";
|
||||||
|
import {wantsDateSeparator} from '../../../DateUtils';
|
||||||
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
|
|
||||||
|
export default class MessageEditHistoryDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
mxEvent: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
events: [],
|
||||||
|
nextBatch: null,
|
||||||
|
isLoading: true,
|
||||||
|
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMoreEdits = async (backwards) => {
|
||||||
|
if (backwards || (!this.state.nextBatch && !this.state.isLoading)) {
|
||||||
|
// bail out on backwards as we only paginate in one direction
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const opts = {from: this.state.nextBatch};
|
||||||
|
const roomId = this.props.mxEvent.getRoomId();
|
||||||
|
const eventId = this.props.mxEvent.getId();
|
||||||
|
const result = await MatrixClientPeg.get().relations(
|
||||||
|
roomId, eventId, "m.replace", "m.room.message", opts);
|
||||||
|
let resolve;
|
||||||
|
const promise = new Promise(r => resolve = r);
|
||||||
|
this.setState({
|
||||||
|
events: this.state.events.concat(result.events),
|
||||||
|
nextBatch: result.nextBatch,
|
||||||
|
isLoading: false,
|
||||||
|
}, () => {
|
||||||
|
const hasMoreResults = !!this.state.nextBatch;
|
||||||
|
resolve(hasMoreResults);
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadMoreEdits();
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderEdits() {
|
||||||
|
const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage');
|
||||||
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
|
const nodes = [];
|
||||||
|
let lastEvent;
|
||||||
|
this.state.events.forEach(e => {
|
||||||
|
if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
|
||||||
|
nodes.push(<li key={e.getTs() + "~"}><DateSeparator ts={e.getTs()} /></li>);
|
||||||
|
}
|
||||||
|
nodes.push(<EditHistoryMessage key={e.getId()} mxEvent={e} isTwelveHour={this.state.isTwelveHour} />);
|
||||||
|
lastEvent = e;
|
||||||
|
});
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let content;
|
||||||
|
if (this.state.error) {
|
||||||
|
content = this.state.error;
|
||||||
|
} else if (this.state.isLoading) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
content = <Spinner />;
|
||||||
|
} else {
|
||||||
|
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||||
|
content = (<ScrollPanel
|
||||||
|
className="mx_MessageEditHistoryDialog_scrollPanel"
|
||||||
|
onFillRequest={ this.loadMoreEdits }
|
||||||
|
stickyBottom={false}
|
||||||
|
startAtBottom={false}
|
||||||
|
>
|
||||||
|
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
|
||||||
|
</ScrollPanel>);
|
||||||
|
}
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
return (
|
||||||
|
<BaseDialog className='mx_MessageEditHistoryDialog' hasCancel={true}
|
||||||
|
onFinished={this.props.onFinished} title={_t("Message edits")}>
|
||||||
|
{content}
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,7 +92,7 @@ export default React.createClass({
|
||||||
<p>
|
<p>
|
||||||
{_t(
|
{_t(
|
||||||
"Upgrading this room requires closing down the current " +
|
"Upgrading this room requires closing down the current " +
|
||||||
"instance of the room and creating a new room it its place. " +
|
"instance of the room and creating a new room in its place. " +
|
||||||
"To give room members the best possible experience, we will:",
|
"To give room members the best possible experience, we will:",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2019 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.
|
||||||
|
@ -18,6 +19,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import filesize from "filesize";
|
||||||
|
|
||||||
export default class UploadConfirmDialog extends React.Component {
|
export default class UploadConfirmDialog extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -49,6 +51,10 @@ export default class UploadConfirmDialog extends React.Component {
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onUploadAllClick = () => {
|
||||||
|
this.props.onFinished(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
@ -71,7 +77,7 @@ export default class UploadConfirmDialog extends React.Component {
|
||||||
preview = <div className="mx_UploadConfirmDialog_previewOuter">
|
preview = <div className="mx_UploadConfirmDialog_previewOuter">
|
||||||
<div className="mx_UploadConfirmDialog_previewInner">
|
<div className="mx_UploadConfirmDialog_previewInner">
|
||||||
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this._objectUrl} /></div>
|
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this._objectUrl} /></div>
|
||||||
<div>{this.props.file.name}</div>
|
<div>{this.props.file.name} ({filesize(this.props.file.size)})</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
|
@ -80,11 +86,18 @@ export default class UploadConfirmDialog extends React.Component {
|
||||||
<img className="mx_UploadConfirmDialog_fileIcon"
|
<img className="mx_UploadConfirmDialog_fileIcon"
|
||||||
src={require("../../../../res/img/files.png")}
|
src={require("../../../../res/img/files.png")}
|
||||||
/>
|
/>
|
||||||
{this.props.file.name}
|
{this.props.file.name} ({filesize(this.props.file.size)})
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let uploadAllButton;
|
||||||
|
if (this.props.currentIndex + 1 < this.props.totalFiles) {
|
||||||
|
uploadAllButton = <button onClick={this._onUploadAllClick}>
|
||||||
|
{_t("Upload all")}
|
||||||
|
</button>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_UploadConfirmDialog'
|
<BaseDialog className='mx_UploadConfirmDialog'
|
||||||
fixedWidth={false}
|
fixedWidth={false}
|
||||||
|
@ -100,7 +113,9 @@ export default class UploadConfirmDialog extends React.Component {
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
onPrimaryButtonClick={this._onUploadClick}
|
onPrimaryButtonClick={this._onUploadClick}
|
||||||
focus={true}
|
focus={true}
|
||||||
/>
|
>
|
||||||
|
{uploadAllButton}
|
||||||
|
</DialogButtons>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,19 +240,13 @@ export default class AppTile extends React.Component {
|
||||||
if (this.props.onEditClick) {
|
if (this.props.onEditClick) {
|
||||||
this.props.onEditClick();
|
this.props.onEditClick();
|
||||||
} else {
|
} else {
|
||||||
|
// The dialog handles scalar auth for us
|
||||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
this._scalarClient.connect().done(() => {
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
room: this.props.room,
|
||||||
this.props.room, 'type_' + this.props.type, this.props.id);
|
screen: 'type_' + this.props.type,
|
||||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
integrationId: this.props.id,
|
||||||
src: src,
|
}, "mx_IntegrationsManager");
|
||||||
}, "mx_IntegrationsManager");
|
|
||||||
}, (err) => {
|
|
||||||
this.setState({
|
|
||||||
error: err.message,
|
|
||||||
});
|
|
||||||
console.error('Error ensuring a valid scalar_token exists', err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
195
src/components/views/elements/InteractiveTooltip.js
Normal file
195
src/components/views/elements/InteractiveTooltip.js
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container";
|
||||||
|
|
||||||
|
// If the distance from tooltip to window edge is below this value, the tooltip
|
||||||
|
// will flip around to the other side of the target.
|
||||||
|
const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20;
|
||||||
|
|
||||||
|
function getOrCreateContainer() {
|
||||||
|
let container = document.getElementById(InteractiveTooltipContainerId);
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement("div");
|
||||||
|
container.id = InteractiveTooltipContainerId;
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInRect(x, y, rect, buffer = 10) {
|
||||||
|
const { top, right, bottom, left } = rect;
|
||||||
|
return x >= (left - buffer) && x <= (right + buffer)
|
||||||
|
&& y >= (top - buffer) && y <= (bottom + buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This style of tooltip takes a "target" element as its child and centers the
|
||||||
|
* tooltip along one edge of the target.
|
||||||
|
*/
|
||||||
|
export default class InteractiveTooltip extends React.Component {
|
||||||
|
propTypes: {
|
||||||
|
// Content to show in the tooltip
|
||||||
|
content: PropTypes.node.isRequired,
|
||||||
|
// Function to call when visibility of the tooltip changes
|
||||||
|
onVisibilityChange: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
contentRect: null,
|
||||||
|
visible: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
// Whenever this passthrough component updates, also render the tooltip
|
||||||
|
// in a separate DOM tree. This allows the tooltip content to participate
|
||||||
|
// the normal React rendering cycle: when this component re-renders, the
|
||||||
|
// tooltip content re-renders.
|
||||||
|
// Once we upgrade to React 16, this could be done a bit more naturally
|
||||||
|
// using the portals feature instead.
|
||||||
|
this.renderTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
collectContentRect = (element) => {
|
||||||
|
// We don't need to clean up when unmounting, so ignore
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
contentRect: element.getBoundingClientRect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
collectTarget = (element) => {
|
||||||
|
this.target = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBackgroundClick = (ev) => {
|
||||||
|
this.hideTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
onBackgroundMouseMove = (ev) => {
|
||||||
|
const { clientX: x, clientY: y } = ev;
|
||||||
|
const { contentRect } = this.state;
|
||||||
|
const targetRect = this.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (!isInRect(x, y, contentRect) && !isInRect(x, y, targetRect)) {
|
||||||
|
this.hideTooltip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTargetMouseOver = (ev) => {
|
||||||
|
this.showTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
showTooltip() {
|
||||||
|
this.setState({
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
if (this.props.onVisibilityChange) {
|
||||||
|
this.props.onVisibilityChange(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideTooltip() {
|
||||||
|
this.setState({
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
if (this.props.onVisibilityChange) {
|
||||||
|
this.props.onVisibilityChange(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTooltip() {
|
||||||
|
const { contentRect, visible } = this.state;
|
||||||
|
if (!visible) {
|
||||||
|
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetRect = this.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||||
|
const targetLeft = targetRect.left + window.pageXOffset;
|
||||||
|
const targetBottom = targetRect.bottom + window.pageYOffset;
|
||||||
|
const targetTop = targetRect.top + window.pageYOffset;
|
||||||
|
|
||||||
|
// Place the tooltip above the target by default. If we find that the
|
||||||
|
// tooltip content would extend past the safe area towards the window
|
||||||
|
// edge, flip around to below the target.
|
||||||
|
const position = {};
|
||||||
|
let chevronFace = null;
|
||||||
|
if (contentRect && (targetTop - contentRect.height <= MIN_SAFE_DISTANCE_TO_WINDOW_EDGE)) {
|
||||||
|
position.top = targetBottom;
|
||||||
|
chevronFace = "top";
|
||||||
|
} else {
|
||||||
|
position.bottom = window.innerHeight - targetTop;
|
||||||
|
chevronFace = "bottom";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center the tooltip horizontally with the target's center.
|
||||||
|
position.left = targetLeft + targetRect.width / 2;
|
||||||
|
|
||||||
|
const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
|
||||||
|
|
||||||
|
const menuClasses = classNames({
|
||||||
|
'mx_InteractiveTooltip': true,
|
||||||
|
'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top',
|
||||||
|
'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom',
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuStyle = {};
|
||||||
|
if (contentRect) {
|
||||||
|
menuStyle.left = `-${contentRect.width / 2}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltip = <div className="mx_InteractiveTooltip_wrapper" style={{...position}}>
|
||||||
|
<div className="mx_ContextualMenu_background"
|
||||||
|
onMouseMove={this.onBackgroundMouseMove}
|
||||||
|
onClick={this.onBackgroundClick}
|
||||||
|
/>
|
||||||
|
<div className={menuClasses}
|
||||||
|
style={menuStyle}
|
||||||
|
ref={this.collectContentRect}
|
||||||
|
>
|
||||||
|
{chevron}
|
||||||
|
{this.props.content}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
ReactDOM.render(tooltip, getOrCreateContainer());
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// We use `cloneElement` here to append some props to the child content
|
||||||
|
// without using a wrapper element which could disrupt layout.
|
||||||
|
return React.cloneElement(this.props.children, {
|
||||||
|
ref: this.collectTarget,
|
||||||
|
onMouseOver: this.onTargetMouseOver,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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,95 +18,34 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import classNames from 'classnames';
|
|
||||||
import SdkConfig from '../../../SdkConfig';
|
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import ScalarMessaging from '../../../ScalarMessaging';
|
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import AccessibleButton from './AccessibleButton';
|
|
||||||
|
|
||||||
export default class ManageIntegsButton extends React.Component {
|
export default class ManageIntegsButton extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
|
||||||
scalarError: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onManageIntegrations = this.onManageIntegrations.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
onManageIntegrations = (ev) => {
|
||||||
ScalarMessaging.startListening();
|
|
||||||
this.scalarClient = null;
|
|
||||||
|
|
||||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
|
||||||
this.scalarClient = new ScalarAuthClient();
|
|
||||||
this.scalarClient.connect().done(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
}, (err) => {
|
|
||||||
this.setState({scalarError: err});
|
|
||||||
console.error('Error whilst initialising scalarClient for ManageIntegsButton', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
ScalarMessaging.stopListening();
|
|
||||||
}
|
|
||||||
|
|
||||||
onManageIntegrations(ev) {
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
this.scalarClient.connect().done(() => {
|
Modal.createDialog(IntegrationsManager, {
|
||||||
Modal.createDialog(IntegrationsManager, {
|
room: this.props.room,
|
||||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
}, "mx_IntegrationsManager");
|
||||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
|
};
|
||||||
null,
|
|
||||||
}, "mx_IntegrationsManager");
|
|
||||||
}, (err) => {
|
|
||||||
this.setState({scalarError: err});
|
|
||||||
console.error('Error ensuring a valid scalar_token exists', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let integrationsButton = <div />;
|
let integrationsButton = <div />;
|
||||||
let integrationsWarningTriangle = <div />;
|
if (ScalarAuthClient.isPossible()) {
|
||||||
let integrationsErrorPopup = <div />;
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
if (this.scalarClient !== null) {
|
|
||||||
const integrationsButtonClasses = classNames({
|
|
||||||
mx_RoomHeader_button: true,
|
|
||||||
mx_RoomHeader_manageIntegsButton: true,
|
|
||||||
mx_ManageIntegsButton_error: !!this.state.scalarError,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
|
||||||
integrationsWarningTriangle = <img
|
|
||||||
src={require("../../../../res/img/warning.svg")}
|
|
||||||
title={_t('Integrations Error')}
|
|
||||||
width="17"
|
|
||||||
/>;
|
|
||||||
// Popup shown when hovering over integrationsButton_error (via CSS)
|
|
||||||
integrationsErrorPopup = (
|
|
||||||
<span className="mx_ManageIntegsButton_errorPopup">
|
|
||||||
{ _t('Could not connect to the integration server') }
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
integrationsButton = (
|
integrationsButton = (
|
||||||
<AccessibleButton className={integrationsButtonClasses}
|
<AccessibleButton
|
||||||
|
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
|
||||||
|
title={_t("Manage Integrations")}
|
||||||
onClick={this.onManageIntegrations}
|
onClick={this.onManageIntegrations}
|
||||||
title={_t('Manage Integrations')}
|
/>
|
||||||
>
|
|
||||||
{ integrationsWarningTriangle }
|
|
||||||
{ integrationsErrorPopup }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,14 @@ export default class MessageEditor extends React.Component {
|
||||||
this.model.update(text, event.inputType, caret);
|
this.model.update(text, event.inputType, caret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_insertText(textToInsert, inputType = "insertText") {
|
||||||
|
const sel = document.getSelection();
|
||||||
|
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
|
||||||
|
const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset);
|
||||||
|
caret.offset += textToInsert.length;
|
||||||
|
this.model.update(newText, inputType, caret);
|
||||||
|
}
|
||||||
|
|
||||||
_isCaretAtStart() {
|
_isCaretAtStart() {
|
||||||
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
|
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
|
||||||
return caret.offset === 0;
|
return caret.offset === 0;
|
||||||
|
@ -92,7 +100,7 @@ export default class MessageEditor extends React.Component {
|
||||||
// insert newline on Shift+Enter
|
// insert newline on Shift+Enter
|
||||||
if (event.shiftKey && event.key === "Enter") {
|
if (event.shiftKey && event.key === "Enter") {
|
||||||
event.preventDefault(); // just in case the browser does support this
|
event.preventDefault(); // just in case the browser does support this
|
||||||
document.execCommand("insertHTML", undefined, "\n");
|
this._insertText("\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
||||||
|
@ -150,16 +158,28 @@ export default class MessageEditor extends React.Component {
|
||||||
dis.dispatch({action: 'focus_composer'});
|
dis.dispatch({action: 'focus_composer'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isEmote() {
|
||||||
|
const firstPart = this.model.parts[0];
|
||||||
|
return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me ");
|
||||||
|
}
|
||||||
|
|
||||||
_sendEdit = () => {
|
_sendEdit = () => {
|
||||||
|
const isEmote = this._isEmote();
|
||||||
|
let model = this.model;
|
||||||
|
if (isEmote) {
|
||||||
|
// trim "/me "
|
||||||
|
model = model.clone();
|
||||||
|
model.removeText({index: 0, offset: 0}, 4);
|
||||||
|
}
|
||||||
const newContent = {
|
const newContent = {
|
||||||
"msgtype": "m.text",
|
"msgtype": isEmote ? "m.emote" : "m.text",
|
||||||
"body": textSerialize(this.model),
|
"body": textSerialize(model),
|
||||||
};
|
};
|
||||||
const contentBody = {
|
const contentBody = {
|
||||||
msgtype: newContent.msgtype,
|
msgtype: newContent.msgtype,
|
||||||
body: ` * ${newContent.body}`,
|
body: ` * ${newContent.body}`,
|
||||||
};
|
};
|
||||||
const formattedBody = htmlSerializeIfNeeded(this.model);
|
const formattedBody = htmlSerializeIfNeeded(model);
|
||||||
if (formattedBody) {
|
if (formattedBody) {
|
||||||
newContent.format = "org.matrix.custom.html";
|
newContent.format = "org.matrix.custom.html";
|
||||||
newContent.formatted_body = formattedBody;
|
newContent.formatted_body = formattedBody;
|
||||||
|
@ -232,7 +252,7 @@ export default class MessageEditor extends React.Component {
|
||||||
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
|
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||||
} else {
|
} else {
|
||||||
// otherwise, parse the body of the event
|
// otherwise, parse the body of the event
|
||||||
parts = parseEvent(editState.getEvent(), room, this.context.matrixClient);
|
parts = parseEvent(editState.getEvent(), partCreator);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EditorModel(
|
return new EditorModel(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd.
|
Copyright 2017 New Vector Ltd.
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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 +19,7 @@ import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'ToolTipButton',
|
displayName: 'TooltipButton',
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
|
@ -41,12 +42,12 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||||
const tip = this.state.hover ? <Tooltip
|
const tip = this.state.hover ? <Tooltip
|
||||||
className="mx_ToolTipButton_container"
|
className="mx_TooltipButton_container"
|
||||||
tooltipClassName="mx_ToolTipButton_helpText"
|
tooltipClassName="mx_TooltipButton_helpText"
|
||||||
label={this.props.helpText}
|
label={this.props.helpText}
|
||||||
/> : <div />;
|
/> : <div />;
|
||||||
return (
|
return (
|
||||||
<div className="mx_ToolTipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
|
<div className="mx_TooltipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
|
||||||
?
|
?
|
||||||
{ tip }
|
{ tip }
|
||||||
</div>
|
</div>
|
|
@ -224,7 +224,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
<div className="mx_MemberInfo_profile">
|
<div className="mx_MemberInfo_profile">
|
||||||
<div className="mx_MemberInfo_profileField">
|
<div className="mx_MemberInfo_profileField">
|
||||||
{ this.state.groupRoom.canonical_alias }
|
{ this.state.groupRoom.canonicalAlias }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
61
src/components/views/messages/EditHistoryMessage.js
Normal file
61
src/components/views/messages/EditHistoryMessage.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 * as HtmlUtils from '../../../HtmlUtils';
|
||||||
|
import {formatTime} from '../../../DateUtils';
|
||||||
|
import {MatrixEvent} from 'matrix-js-sdk';
|
||||||
|
import {pillifyLinks} from '../../../utils/pillify';
|
||||||
|
|
||||||
|
export default class EditHistoryMessage extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
// the message event being edited
|
||||||
|
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {mxEvent} = this.props;
|
||||||
|
const originalContent = mxEvent.getOriginalContent();
|
||||||
|
const content = originalContent["m.new_content"] || originalContent;
|
||||||
|
const contentElements = HtmlUtils.bodyToHtml(content);
|
||||||
|
let contentContainer;
|
||||||
|
if (mxEvent.getContent().msgtype === "m.emote") {
|
||||||
|
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||||
|
contentContainer = (<div className="mx_EventTile_content" ref="content">*
|
||||||
|
<span className="mx_MEmoteBody_sender">{ name }</span>
|
||||||
|
{contentElements}
|
||||||
|
</div>);
|
||||||
|
} else {
|
||||||
|
contentContainer = (<div className="mx_EventTile_content" ref="content">{contentElements}</div>);
|
||||||
|
}
|
||||||
|
const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour);
|
||||||
|
return <li className="mx_EventTile">
|
||||||
|
<div className="mx_EventTile_line">
|
||||||
|
<span className="mx_MessageTimestamp">{timestamp}</span>
|
||||||
|
{ contentContainer }
|
||||||
|
</div>
|
||||||
|
</li>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,7 +57,7 @@ export default class MessageActionBar extends React.PureComponent {
|
||||||
this.props.onFocusChange(focused);
|
this.props.onFocusChange(focused);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCryptoClicked = () => {
|
onCryptoClick = () => {
|
||||||
const event = this.props.mxEvent;
|
const event = this.props.mxEvent;
|
||||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||||
|
@ -89,7 +89,7 @@ export default class MessageActionBar extends React.PureComponent {
|
||||||
|
|
||||||
let e2eInfoCallback = null;
|
let e2eInfoCallback = null;
|
||||||
if (this.props.mxEvent.isEncrypted()) {
|
if (this.props.mxEvent.isEncrypted()) {
|
||||||
e2eInfoCallback = () => this.onCryptoClicked();
|
e2eInfoCallback = () => this.onCryptoClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuOptions = {
|
const menuOptions = {
|
||||||
|
@ -131,43 +131,28 @@ export default class MessageActionBar extends React.PureComponent {
|
||||||
return SettingsStore.isFeatureEnabled("feature_message_editing");
|
return SettingsStore.isFeatureEnabled("feature_message_editing");
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAgreeDimension() {
|
renderReactButton() {
|
||||||
if (!this.isReactionsEnabled()) {
|
if (!this.isReactionsEnabled()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
|
const ReactMessageAction = sdk.getComponent('messages.ReactMessageAction');
|
||||||
return <ReactionDimension
|
const { mxEvent, reactions } = this.props;
|
||||||
title={_t("Agree or Disagree")}
|
|
||||||
options={["👍", "👎"]}
|
|
||||||
reactions={this.props.reactions}
|
|
||||||
mxEvent={this.props.mxEvent}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLikeDimension() {
|
return <ReactMessageAction
|
||||||
if (!this.isReactionsEnabled()) {
|
mxEvent={mxEvent}
|
||||||
return null;
|
reactions={reactions}
|
||||||
}
|
onFocusChange={this.onFocusChange}
|
||||||
|
|
||||||
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
|
|
||||||
return <ReactionDimension
|
|
||||||
title={_t("Like or Dislike")}
|
|
||||||
options={["🙂", "😔"]}
|
|
||||||
reactions={this.props.reactions}
|
|
||||||
mxEvent={this.props.mxEvent}
|
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let agreeDimensionReactionButtons;
|
let reactButton;
|
||||||
let likeDimensionReactionButtons;
|
|
||||||
let replyButton;
|
let replyButton;
|
||||||
let editButton;
|
let editButton;
|
||||||
|
|
||||||
if (isContentActionable(this.props.mxEvent)) {
|
if (isContentActionable(this.props.mxEvent)) {
|
||||||
agreeDimensionReactionButtons = this.renderAgreeDimension();
|
reactButton = this.renderReactButton();
|
||||||
likeDimensionReactionButtons = this.renderLikeDimension();
|
|
||||||
replyButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
replyButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||||
title={_t("Reply")}
|
title={_t("Reply")}
|
||||||
onClick={this.onReplyClick}
|
onClick={this.onReplyClick}
|
||||||
|
@ -181,8 +166,7 @@ export default class MessageActionBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_MessageActionBar">
|
return <div className="mx_MessageActionBar">
|
||||||
{agreeDimensionReactionButtons}
|
{reactButton}
|
||||||
{likeDimensionReactionButtons}
|
|
||||||
{replyButton}
|
{replyButton}
|
||||||
{editButton}
|
{editButton}
|
||||||
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
|
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
|
||||||
|
|
97
src/components/views/messages/ReactMessageAction.js
Normal file
97
src/components/views/messages/ReactMessageAction.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default class ReactMessageAction extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
mxEvent: PropTypes.object.isRequired,
|
||||||
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
|
reactions: PropTypes.object,
|
||||||
|
onFocusChange: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
if (props.reactions) {
|
||||||
|
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||||
|
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||||
|
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusChange = (focused) => {
|
||||||
|
if (!this.props.onFocusChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onFocusChange(focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.reactions !== this.props.reactions) {
|
||||||
|
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||||
|
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||||
|
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||||
|
this.onReactionsChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.props.reactions) {
|
||||||
|
this.props.reactions.removeListener(
|
||||||
|
"Relations.add",
|
||||||
|
this.onReactionsChange,
|
||||||
|
);
|
||||||
|
this.props.reactions.removeListener(
|
||||||
|
"Relations.remove",
|
||||||
|
this.onReactionsChange,
|
||||||
|
);
|
||||||
|
this.props.reactions.removeListener(
|
||||||
|
"Relations.redaction",
|
||||||
|
this.onReactionsChange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReactionsChange = () => {
|
||||||
|
// Force a re-render of the tooltip because a change in the reactions
|
||||||
|
// set means the event tile's layout may have changed and possibly
|
||||||
|
// altered the location where the tooltip should be shown.
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const ReactionsQuickTooltip = sdk.getComponent('messages.ReactionsQuickTooltip');
|
||||||
|
const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip');
|
||||||
|
const { mxEvent, reactions } = this.props;
|
||||||
|
|
||||||
|
const content = <ReactionsQuickTooltip
|
||||||
|
mxEvent={mxEvent}
|
||||||
|
reactions={reactions}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
return <InteractiveTooltip
|
||||||
|
content={content}
|
||||||
|
onVisibilityChange={this.onFocusChange}
|
||||||
|
>
|
||||||
|
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
|
||||||
|
</InteractiveTooltip>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,176 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 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 classNames from 'classnames';
|
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
|
||||||
|
|
||||||
export default class ReactionDimension extends React.PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
mxEvent: PropTypes.object.isRequired,
|
|
||||||
// Array of strings containing the emoji for each option
|
|
||||||
options: PropTypes.array.isRequired,
|
|
||||||
title: PropTypes.string,
|
|
||||||
// The Relations model from the JS SDK for reactions
|
|
||||||
reactions: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = this.getSelection();
|
|
||||||
|
|
||||||
if (props.reactions) {
|
|
||||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
|
||||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
|
||||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.reactions !== this.props.reactions) {
|
|
||||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
|
||||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
|
||||||
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
|
||||||
this.onReactionsChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.props.reactions) {
|
|
||||||
this.props.reactions.removeListener(
|
|
||||||
"Relations.add",
|
|
||||||
this.onReactionsChange,
|
|
||||||
);
|
|
||||||
this.props.reactions.removeListener(
|
|
||||||
"Relations.remove",
|
|
||||||
this.onReactionsChange,
|
|
||||||
);
|
|
||||||
this.props.reactions.removeListener(
|
|
||||||
"Relations.redaction",
|
|
||||||
this.onReactionsChange,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onReactionsChange = () => {
|
|
||||||
this.setState(this.getSelection());
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelection() {
|
|
||||||
const myReactions = this.getMyReactions();
|
|
||||||
if (!myReactions) {
|
|
||||||
return {
|
|
||||||
selectedOption: null,
|
|
||||||
selectedReactionEvent: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { options } = this.props;
|
|
||||||
let selectedOption = null;
|
|
||||||
let selectedReactionEvent = null;
|
|
||||||
for (const option of options) {
|
|
||||||
const reactionForOption = myReactions.find(mxEvent => {
|
|
||||||
if (mxEvent.isRedacted()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return mxEvent.getRelation().key === option;
|
|
||||||
});
|
|
||||||
if (!reactionForOption) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (selectedOption) {
|
|
||||||
// If there are multiple selected values (only expected to occur via
|
|
||||||
// non-Riot clients), then act as if none are selected.
|
|
||||||
return {
|
|
||||||
selectedOption: null,
|
|
||||||
selectedReactionEvent: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
selectedOption = option;
|
|
||||||
selectedReactionEvent = reactionForOption;
|
|
||||||
}
|
|
||||||
return { selectedOption, selectedReactionEvent };
|
|
||||||
}
|
|
||||||
|
|
||||||
getMyReactions() {
|
|
||||||
const reactions = this.props.reactions;
|
|
||||||
if (!reactions) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const userId = MatrixClientPeg.get().getUserId();
|
|
||||||
const myReactions = reactions.getAnnotationsBySender()[userId];
|
|
||||||
if (!myReactions) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return [...myReactions.values()];
|
|
||||||
}
|
|
||||||
|
|
||||||
onOptionClick = (ev) => {
|
|
||||||
const { key } = ev.target.dataset;
|
|
||||||
this.toggleDimension(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDimension(key) {
|
|
||||||
const { selectedOption, selectedReactionEvent } = this.state;
|
|
||||||
const newSelectedOption = selectedOption !== key ? key : null;
|
|
||||||
this.setState({
|
|
||||||
selectedOption: newSelectedOption,
|
|
||||||
});
|
|
||||||
if (selectedReactionEvent) {
|
|
||||||
MatrixClientPeg.get().redactEvent(
|
|
||||||
this.props.mxEvent.getRoomId(),
|
|
||||||
selectedReactionEvent.getId(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (newSelectedOption) {
|
|
||||||
MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", {
|
|
||||||
"m.relates_to": {
|
|
||||||
"rel_type": "m.annotation",
|
|
||||||
"event_id": this.props.mxEvent.getId(),
|
|
||||||
"key": newSelectedOption,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { selectedOption } = this.state;
|
|
||||||
const { options } = this.props;
|
|
||||||
|
|
||||||
const items = options.map(option => {
|
|
||||||
const disabled = selectedOption && selectedOption !== option;
|
|
||||||
const classes = classNames({
|
|
||||||
mx_ReactionDimension_disabled: disabled,
|
|
||||||
});
|
|
||||||
return <span key={option}
|
|
||||||
data-key={option}
|
|
||||||
className={classes}
|
|
||||||
onClick={this.onOptionClick}
|
|
||||||
>
|
|
||||||
{option}
|
|
||||||
</span>;
|
|
||||||
});
|
|
||||||
|
|
||||||
return <span className="mx_ReactionDimension"
|
|
||||||
title={this.props.title}
|
|
||||||
aria-hidden={true}
|
|
||||||
>
|
|
||||||
{items}
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
}
|
|
68
src/components/views/messages/ReactionTooltipButton.js
Normal file
68
src/components/views/messages/ReactionTooltipButton.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 classNames from 'classnames';
|
||||||
|
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
|
export default class ReactionTooltipButton extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
mxEvent: PropTypes.object.isRequired,
|
||||||
|
// The reaction content / key / emoji
|
||||||
|
content: PropTypes.string.isRequired,
|
||||||
|
title: PropTypes.string,
|
||||||
|
// A possible Matrix event if the current user has voted for this type
|
||||||
|
myReactionEvent: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
onClick = (ev) => {
|
||||||
|
const { mxEvent, myReactionEvent, content } = this.props;
|
||||||
|
if (myReactionEvent) {
|
||||||
|
MatrixClientPeg.get().redactEvent(
|
||||||
|
mxEvent.getRoomId(),
|
||||||
|
myReactionEvent.getId(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", {
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.annotation",
|
||||||
|
"event_id": mxEvent.getId(),
|
||||||
|
"key": content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { content, myReactionEvent } = this.props;
|
||||||
|
|
||||||
|
const classes = classNames({
|
||||||
|
mx_ReactionTooltipButton: true,
|
||||||
|
mx_ReactionTooltipButton_selected: !!myReactionEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <span className={classes}
|
||||||
|
data-key={content}
|
||||||
|
title={this.props.title}
|
||||||
|
aria-hidden={true}
|
||||||
|
onClick={this.onClick}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
}
|
195
src/components/views/messages/ReactionsQuickTooltip.js
Normal file
195
src/components/views/messages/ReactionsQuickTooltip.js
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { _t } from '../../../languageHandler';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import { unicodeToShortcode } from '../../../HtmlUtils';
|
||||||
|
|
||||||
|
export default class ReactionsQuickTooltip extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
mxEvent: PropTypes.object.isRequired,
|
||||||
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
|
reactions: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
if (props.reactions) {
|
||||||
|
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||||
|
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||||
|
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hoveredItem: null,
|
||||||
|
myReactions: this.getMyReactions(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.reactions !== this.props.reactions) {
|
||||||
|
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||||
|
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||||
|
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||||
|
this.onReactionsChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.props.reactions) {
|
||||||
|
this.props.reactions.removeListener(
|
||||||
|
"Relations.add",
|
||||||
|
this.onReactionsChange,
|
||||||
|
);
|
||||||
|
this.props.reactions.removeListener(
|
||||||
|
"Relations.remove",
|
||||||
|
this.onReactionsChange,
|
||||||
|
);
|
||||||
|
this.props.reactions.removeListener(
|
||||||
|
"Relations.redaction",
|
||||||
|
this.onReactionsChange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReactionsChange = () => {
|
||||||
|
this.setState({
|
||||||
|
myReactions: this.getMyReactions(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getMyReactions() {
|
||||||
|
const reactions = this.props.reactions;
|
||||||
|
if (!reactions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const userId = MatrixClientPeg.get().getUserId();
|
||||||
|
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||||
|
if (!myReactions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [...myReactions.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseOver = (ev) => {
|
||||||
|
const { key } = ev.target.dataset;
|
||||||
|
const item = this.items.find(({ content }) => content === key);
|
||||||
|
this.setState({
|
||||||
|
hoveredItem: item,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseOut = (ev) => {
|
||||||
|
this.setState({
|
||||||
|
hoveredItem: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get items() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
content: "👍",
|
||||||
|
title: _t("Agree"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "👎",
|
||||||
|
title: _t("Disagree"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "😄",
|
||||||
|
title: _t("Happy"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "🎉",
|
||||||
|
title: _t("Party Popper"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "😕",
|
||||||
|
title: _t("Confused"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "❤️",
|
||||||
|
title: _t("Heart"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "🚀",
|
||||||
|
title: _t("Rocket"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "👀",
|
||||||
|
title: _t("Eyes"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { mxEvent } = this.props;
|
||||||
|
const { myReactions, hoveredItem } = this.state;
|
||||||
|
const ReactionTooltipButton = sdk.getComponent('messages.ReactionTooltipButton');
|
||||||
|
|
||||||
|
const buttons = this.items.map(({ content, title }) => {
|
||||||
|
const myReactionEvent = myReactions && myReactions.find(mxEvent => {
|
||||||
|
if (mxEvent.isRedacted()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return mxEvent.getRelation().key === content;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <ReactionTooltipButton
|
||||||
|
key={content}
|
||||||
|
content={content}
|
||||||
|
title={title}
|
||||||
|
mxEvent={mxEvent}
|
||||||
|
myReactionEvent={myReactionEvent}
|
||||||
|
/>;
|
||||||
|
});
|
||||||
|
|
||||||
|
let label = " "; // non-breaking space to keep layout the same when empty
|
||||||
|
if (hoveredItem) {
|
||||||
|
const { content, title } = hoveredItem;
|
||||||
|
|
||||||
|
let shortcodeLabel;
|
||||||
|
const shortcode = unicodeToShortcode(content);
|
||||||
|
if (shortcode) {
|
||||||
|
shortcodeLabel = <span className="mx_ReactionsQuickTooltip_shortcode">
|
||||||
|
{shortcode}
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
label = <div className="mx_ReactionsQuickTooltip_label">
|
||||||
|
<span className="mx_ReactionsQuickTooltip_title">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
{shortcodeLabel}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_ReactionsQuickTooltip"
|
||||||
|
onMouseOver={this.onMouseOver}
|
||||||
|
onMouseOut={this.onMouseOut}
|
||||||
|
>
|
||||||
|
<div className="mx_ReactionsQuickTooltip_buttons">
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
{label}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,10 +18,14 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
import { isContentActionable } from '../../../utils/EventUtils';
|
import { isContentActionable } from '../../../utils/EventUtils';
|
||||||
import { isSingleEmoji } from '../../../HtmlUtils';
|
import { isSingleEmoji } from '../../../HtmlUtils';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
|
// The maximum number of reactions to initially show on a message.
|
||||||
|
const MAX_ITEMS_WHEN_LIMITED = 8;
|
||||||
|
|
||||||
export default class ReactionsRow extends React.PureComponent {
|
export default class ReactionsRow extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
// The event we're displaying reactions for
|
// The event we're displaying reactions for
|
||||||
|
@ -41,6 +45,7 @@ export default class ReactionsRow extends React.PureComponent {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
myReactions: this.getMyReactions(),
|
myReactions: this.getMyReactions(),
|
||||||
|
showAll: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,16 +99,22 @@ export default class ReactionsRow extends React.PureComponent {
|
||||||
return [...myReactions.values()];
|
return [...myReactions.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onShowAllClick = () => {
|
||||||
|
this.setState({
|
||||||
|
showAll: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { mxEvent, reactions } = this.props;
|
const { mxEvent, reactions } = this.props;
|
||||||
const { myReactions } = this.state;
|
const { myReactions, showAll } = this.state;
|
||||||
|
|
||||||
if (!reactions || !isContentActionable(mxEvent)) {
|
if (!reactions || !isContentActionable(mxEvent)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
|
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
|
||||||
const items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||||
if (!isSingleEmoji(content)) {
|
if (!isSingleEmoji(content)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -125,10 +136,26 @@ export default class ReactionsRow extends React.PureComponent {
|
||||||
reactionEvents={events}
|
reactionEvents={events}
|
||||||
myReactionEvent={myReactionEvent}
|
myReactionEvent={myReactionEvent}
|
||||||
/>;
|
/>;
|
||||||
});
|
}).filter(item => !!item);
|
||||||
|
|
||||||
|
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
|
||||||
|
// The "+ 1" ensure that the "show all" reveals something that takes up
|
||||||
|
// more space than the button itself.
|
||||||
|
let showAllButton;
|
||||||
|
if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) {
|
||||||
|
items = items.slice(0, MAX_ITEMS_WHEN_LIMITED);
|
||||||
|
showAllButton = <a
|
||||||
|
className="mx_ReactionsRow_showAll"
|
||||||
|
href="#"
|
||||||
|
onClick={this.onShowAllClick}
|
||||||
|
>
|
||||||
|
{_t("Show all")}
|
||||||
|
</a>;
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="mx_ReactionsRow">
|
return <div className="mx_ReactionsRow">
|
||||||
{items}
|
{items}
|
||||||
|
{showAllButton}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,12 +30,11 @@ import Modal from '../../../Modal';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
|
||||||
import * as ContextualMenu from '../../structures/ContextualMenu';
|
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 ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import {host as matrixtoHost} from '../../../matrix-to';
|
import {host as matrixtoHost} from '../../../matrix-to';
|
||||||
|
import {pillifyLinks} from '../../../utils/pillify';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'TextualBody',
|
displayName: 'TextualBody',
|
||||||
|
@ -99,7 +98,7 @@ module.exports = React.createClass({
|
||||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||||
this.pillifyLinks(this.refs.content.children);
|
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||||
HtmlUtils.linkifyElement(this.refs.content);
|
HtmlUtils.linkifyElement(this.refs.content);
|
||||||
this.calculateUrlPreview();
|
this.calculateUrlPreview();
|
||||||
|
|
||||||
|
@ -184,98 +183,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
pillifyLinks: function(nodes) {
|
|
||||||
const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
|
|
||||||
let node = nodes[0];
|
|
||||||
while (node) {
|
|
||||||
let pillified = false;
|
|
||||||
|
|
||||||
if (node.tagName === "A" && node.getAttribute("href")) {
|
|
||||||
const href = node.getAttribute("href");
|
|
||||||
|
|
||||||
// If the link is a (localised) matrix.to link, replace it with a pill
|
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
|
||||||
if (Pill.isMessagePillUrl(href)) {
|
|
||||||
const pillContainer = document.createElement('span');
|
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
|
||||||
const pill = <Pill
|
|
||||||
url={href}
|
|
||||||
inMessage={true}
|
|
||||||
room={room}
|
|
||||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
ReactDOM.render(pill, pillContainer);
|
|
||||||
node.parentNode.replaceChild(pillContainer, node);
|
|
||||||
// Pills within pills aren't going to go well, so move on
|
|
||||||
pillified = true;
|
|
||||||
|
|
||||||
// update the current node with one that's now taken its place
|
|
||||||
node = pillContainer;
|
|
||||||
}
|
|
||||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
|
||||||
|
|
||||||
let currentTextNode = node;
|
|
||||||
const roomNotifTextNodes = [];
|
|
||||||
|
|
||||||
// Take a textNode and break it up to make all the instances of @room their
|
|
||||||
// own textNode, adding those nodes to roomNotifTextNodes
|
|
||||||
while (currentTextNode !== null) {
|
|
||||||
const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent);
|
|
||||||
let nextTextNode = null;
|
|
||||||
if (roomNotifPos > -1) {
|
|
||||||
let roomTextNode = currentTextNode;
|
|
||||||
|
|
||||||
if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos);
|
|
||||||
if (roomTextNode.textContent.length > Pill.roomNotifLen()) {
|
|
||||||
nextTextNode = roomTextNode.splitText(Pill.roomNotifLen());
|
|
||||||
}
|
|
||||||
roomNotifTextNodes.push(roomTextNode);
|
|
||||||
}
|
|
||||||
currentTextNode = nextTextNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (roomNotifTextNodes.length > 0) {
|
|
||||||
const pushProcessor = new PushProcessor(MatrixClientPeg.get());
|
|
||||||
const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif");
|
|
||||||
if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) {
|
|
||||||
// Now replace all those nodes with Pills
|
|
||||||
for (const roomNotifTextNode of roomNotifTextNodes) {
|
|
||||||
// Set the next node to be processed to the one after the node
|
|
||||||
// we're adding now, since we've just inserted nodes into the structure
|
|
||||||
// we're iterating over.
|
|
||||||
// Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once
|
|
||||||
node = roomNotifTextNode.nextSibling;
|
|
||||||
|
|
||||||
const pillContainer = document.createElement('span');
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
|
||||||
const pill = <Pill
|
|
||||||
type={Pill.TYPE_AT_ROOM_MENTION}
|
|
||||||
inMessage={true}
|
|
||||||
room={room}
|
|
||||||
shouldShowPillAvatar={true}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
ReactDOM.render(pill, pillContainer);
|
|
||||||
roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode);
|
|
||||||
}
|
|
||||||
// Nothing else to do for a text node (and we don't need to advance
|
|
||||||
// the loop pointer because we did it above)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.childNodes && node.childNodes.length && !pillified) {
|
|
||||||
this.pillifyLinks(node.childNodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
node = node.nextSibling;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
findLinks: function(nodes) {
|
findLinks: function(nodes) {
|
||||||
let links = [];
|
let links = [];
|
||||||
|
|
||||||
|
@ -448,6 +355,11 @@ module.exports = React.createClass({
|
||||||
this.setState({editedMarkerHovered: false});
|
this.setState({editedMarkerHovered: false});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_openHistoryDialog: async function() {
|
||||||
|
const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
|
||||||
|
Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent});
|
||||||
|
},
|
||||||
|
|
||||||
_renderEditedMarker: function() {
|
_renderEditedMarker: function() {
|
||||||
let editedTooltip;
|
let editedTooltip;
|
||||||
if (this.state.editedMarkerHovered) {
|
if (this.state.editedMarkerHovered) {
|
||||||
|
@ -456,12 +368,13 @@ module.exports = React.createClass({
|
||||||
const date = editEvent && formatDate(editEvent.getDate());
|
const date = editEvent && formatDate(editEvent.getDate());
|
||||||
editedTooltip = <Tooltip
|
editedTooltip = <Tooltip
|
||||||
tooltipClassName="mx_Tooltip_timeline"
|
tooltipClassName="mx_Tooltip_timeline"
|
||||||
label={_t("Edited at %(date)s", {date})}
|
label={_t("Edited at %(date)s. Click to view edits.", {date})}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key="editedMarker" className="mx_EventTile_edited"
|
key="editedMarker" className="mx_EventTile_edited"
|
||||||
|
onClick={this._openHistoryDialog}
|
||||||
onMouseEnter={this._onMouseEnterEditedMarker}
|
onMouseEnter={this._onMouseEnterEditedMarker}
|
||||||
onMouseLeave={this._onMouseLeaveEditedMarker}
|
onMouseLeave={this._onMouseLeaveEditedMarker}
|
||||||
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
|
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
|
||||||
|
|
|
@ -24,8 +24,6 @@ import AppTile from '../elements/AppTile';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
|
||||||
import ScalarMessaging from '../../../ScalarMessaging';
|
import ScalarMessaging from '../../../ScalarMessaging';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
|
@ -63,20 +61,6 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.scalarClient = null;
|
|
||||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
|
||||||
this.scalarClient = new ScalarAuthClient();
|
|
||||||
this.scalarClient.connect().then(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
}).catch((e) => {
|
|
||||||
console.log('Failed to connect to integrations server');
|
|
||||||
// TODO -- Handle Scalar errors
|
|
||||||
// this.setState({
|
|
||||||
// scalar_error: err,
|
|
||||||
// });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -144,16 +128,10 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_launchManageIntegrations: function() {
|
_launchManageIntegrations: function() {
|
||||||
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
|
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
|
||||||
this.scalarClient.connect().done(() => {
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
room: this.props.room,
|
||||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') :
|
screen: 'add_integ',
|
||||||
null;
|
}, 'mx_IntegrationsManager');
|
||||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
|
||||||
src: src,
|
|
||||||
}, 'mx_IntegrationsManager');
|
|
||||||
}, (err) => {
|
|
||||||
console.error('Error ensuring a valid scalar_token exists', err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onClickAddWidget: function(e) {
|
onClickAddWidget: function(e) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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.
|
||||||
|
@ -403,7 +404,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onCryptoClicked: function(e) {
|
onCryptoClick: function(e) {
|
||||||
const event = this.props.mxEvent;
|
const event = this.props.mxEvent;
|
||||||
|
|
||||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||||
|
@ -439,7 +440,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
|
|
||||||
_renderE2EPadlock: function() {
|
_renderE2EPadlock: function() {
|
||||||
const ev = this.props.mxEvent;
|
const ev = this.props.mxEvent;
|
||||||
const props = {onClick: this.onCryptoClicked};
|
const props = {onClick: this.onCryptoClick};
|
||||||
|
|
||||||
// event could not be decrypted
|
// event could not be decrypted
|
||||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||||
|
@ -670,13 +671,13 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
{'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a>},
|
{'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a>},
|
||||||
);
|
);
|
||||||
|
|
||||||
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
|
const TooltipButton = sdk.getComponent('elements.TooltipButton');
|
||||||
const keyRequestInfo = isEncryptionFailure ?
|
const keyRequestInfo = isEncryptionFailure ?
|
||||||
<div className="mx_EventTile_keyRequestInfo">
|
<div className="mx_EventTile_keyRequestInfo">
|
||||||
<span className="mx_EventTile_keyRequestInfo_text">
|
<span className="mx_EventTile_keyRequestInfo_text">
|
||||||
{ keyRequestInfoContent }
|
{ keyRequestInfoContent }
|
||||||
</span>
|
</span>
|
||||||
<ToolTipButton helpText={keyRequestHelpText} />
|
<TooltipButton helpText={keyRequestHelpText} />
|
||||||
</div> : null;
|
</div> : null;
|
||||||
|
|
||||||
let reactionsRow;
|
let reactionsRow;
|
||||||
|
@ -828,7 +829,7 @@ module.exports.haveTileForEvent = function(e) {
|
||||||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||||
|
|
||||||
// No tile for replacement events since they update the original tile
|
// No tile for replacement events since they update the original tile
|
||||||
if (e.isRelation("m.replace")) return false;
|
if (e.isRelation("m.replace") && SettingsStore.isFeatureEnabled("feature_message_editing")) return false;
|
||||||
|
|
||||||
const handler = getHandlerTile(e);
|
const handler = getHandlerTile(e);
|
||||||
if (handler === undefined) return false;
|
if (handler === undefined) return false;
|
||||||
|
|
|
@ -60,6 +60,7 @@ import ReplyThread from "../elements/ReplyThread";
|
||||||
import {ContentHelpers} from 'matrix-js-sdk';
|
import {ContentHelpers} from 'matrix-js-sdk';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||||
|
import ComposerHistoryManager from "../../../ComposerHistoryManager";
|
||||||
|
|
||||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||||
|
|
||||||
|
@ -140,6 +141,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
autocomplete: Autocomplete;
|
autocomplete: Autocomplete;
|
||||||
|
historyManager: ComposerHistoryManager;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -329,6 +331,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -679,14 +682,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
if (this.autocomplete.countCompletions() > 0) {
|
if (this.autocomplete.countCompletions() > 0) {
|
||||||
if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) {
|
if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) {
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.LEFT:
|
|
||||||
this.autocomplete.moveSelection(-1);
|
|
||||||
ev.preventDefault();
|
|
||||||
return true;
|
|
||||||
case KeyCode.RIGHT:
|
|
||||||
this.autocomplete.moveSelection(+1);
|
|
||||||
ev.preventDefault();
|
|
||||||
return true;
|
|
||||||
case KeyCode.UP:
|
case KeyCode.UP:
|
||||||
this.autocomplete.moveSelection(-1);
|
this.autocomplete.moveSelection(-1);
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -1062,6 +1057,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (!cmd.error) {
|
if (!cmd.error) {
|
||||||
|
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState: this.createEditorState(),
|
editorState: this.createEditorState(),
|
||||||
}, ()=>{
|
}, ()=>{
|
||||||
|
@ -1139,6 +1135,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||||
let sendTextFn = ContentHelpers.makeTextMessage;
|
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||||
|
|
||||||
|
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||||
|
|
||||||
if (commandText && commandText.startsWith('/me')) {
|
if (commandText && commandText.startsWith('/me')) {
|
||||||
if (replyingToEv) {
|
if (replyingToEv) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -1198,19 +1196,31 @@ export default class MessageComposerInput extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
onVerticalArrow = (e, up) => {
|
onVerticalArrow = (e, up) => {
|
||||||
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
|
if (e.ctrlKey || e.shiftKey || e.metaKey) return;
|
||||||
|
|
||||||
// Select history
|
|
||||||
const selection = this.state.editorState.selection;
|
|
||||||
|
|
||||||
// selection must be collapsed
|
// selection must be collapsed
|
||||||
|
const selection = this.state.editorState.selection;
|
||||||
if (!selection.isCollapsed) return;
|
if (!selection.isCollapsed) return;
|
||||||
const document = this.state.editorState.document;
|
|
||||||
|
|
||||||
// and we must be at the edge of the document (up=start, down=end)
|
// and we must be at the edge of the document (up=start, down=end)
|
||||||
|
const document = this.state.editorState.document;
|
||||||
if (up) {
|
if (up) {
|
||||||
if (!selection.anchor.isAtStartOfNode(document)) return;
|
if (!selection.anchor.isAtStartOfNode(document)) return;
|
||||||
|
} else {
|
||||||
|
if (!selection.anchor.isAtEndOfNode(document)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing");
|
||||||
|
const shouldSelectHistory = (editingEnabled && e.altKey) || !editingEnabled;
|
||||||
|
const shouldEditLastMessage = editingEnabled && !e.altKey && up;
|
||||||
|
|
||||||
|
if (shouldSelectHistory) {
|
||||||
|
// Try select composer history
|
||||||
|
const selected = this.selectHistory(up);
|
||||||
|
if (selected) {
|
||||||
|
// We're selecting history, so prevent the key event from doing anything else
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if (shouldEditLastMessage) {
|
||||||
const editEvent = findEditableEvent(this.props.room, false);
|
const editEvent = findEditableEvent(this.props.room, false);
|
||||||
if (editEvent) {
|
if (editEvent) {
|
||||||
// We're selecting history, so prevent the key event from doing anything else
|
// We're selecting history, so prevent the key event from doing anything else
|
||||||
|
@ -1223,6 +1233,54 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectHistory = (up) => {
|
||||||
|
const delta = up ? -1 : 1;
|
||||||
|
|
||||||
|
// True if we are not currently selecting history, but composing a message
|
||||||
|
if (this.historyManager.currentIndex === this.historyManager.history.length) {
|
||||||
|
// We can't go any further - there isn't any more history, so nop.
|
||||||
|
if (!up) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
currentlyComposedEditorState: this.state.editorState,
|
||||||
|
});
|
||||||
|
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
|
||||||
|
// True when we return to the message being composed currently
|
||||||
|
this.setState({
|
||||||
|
editorState: this.state.currentlyComposedEditorState,
|
||||||
|
});
|
||||||
|
this.historyManager.currentIndex = this.historyManager.history.length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let editorState;
|
||||||
|
const historyItem = this.historyManager.getItem(delta);
|
||||||
|
if (!historyItem) return;
|
||||||
|
|
||||||
|
if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) {
|
||||||
|
editorState = this.richToMdEditorState(historyItem.value);
|
||||||
|
} else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) {
|
||||||
|
editorState = this.mdToRichEditorState(historyItem.value);
|
||||||
|
} else {
|
||||||
|
editorState = historyItem.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move selection to the end of the selected history
|
||||||
|
const change = editorState.change().moveToEndOfNode(editorState.document);
|
||||||
|
|
||||||
|
// We don't call this.onChange(change) now, as fixups on stuff like pills
|
||||||
|
// should already have been done and persisted in the history.
|
||||||
|
editorState = change.value;
|
||||||
|
|
||||||
|
this.suppressAutoComplete = true;
|
||||||
|
|
||||||
|
this.setState({ editorState }, ()=>{
|
||||||
|
this._editor.focus();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
onTab = async (e) => {
|
onTab = async (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
someCompletions: null,
|
someCompletions: null,
|
||||||
|
|
|
@ -66,6 +66,7 @@ module.exports = React.createClass({
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
|
|
||||||
canPreview: PropTypes.bool,
|
canPreview: PropTypes.bool,
|
||||||
|
previewLoading: PropTypes.bool,
|
||||||
room: PropTypes.object,
|
room: PropTypes.object,
|
||||||
|
|
||||||
// When a spinner is present, a spinnerState can be specified to indicate the
|
// When a spinner is present, a spinnerState can be specified to indicate the
|
||||||
|
@ -254,6 +255,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
|
||||||
let showSpinner = false;
|
let showSpinner = false;
|
||||||
let darkStyle = false;
|
let darkStyle = false;
|
||||||
let title;
|
let title;
|
||||||
|
@ -262,6 +265,7 @@ module.exports = React.createClass({
|
||||||
let primaryActionLabel;
|
let primaryActionLabel;
|
||||||
let secondaryActionHandler;
|
let secondaryActionHandler;
|
||||||
let secondaryActionLabel;
|
let secondaryActionLabel;
|
||||||
|
let footer;
|
||||||
|
|
||||||
const messageCase = this._getMessageCase();
|
const messageCase = this._getMessageCase();
|
||||||
switch (messageCase) {
|
switch (messageCase) {
|
||||||
|
@ -287,6 +291,14 @@ module.exports = React.createClass({
|
||||||
primaryActionHandler = this.onRegisterClick;
|
primaryActionHandler = this.onRegisterClick;
|
||||||
secondaryActionLabel = _t("Sign In");
|
secondaryActionLabel = _t("Sign In");
|
||||||
secondaryActionHandler = this.onLoginClick;
|
secondaryActionHandler = this.onLoginClick;
|
||||||
|
if (this.props.previewLoading) {
|
||||||
|
footer = (
|
||||||
|
<div>
|
||||||
|
<Spinner w={20} h={20}/>
|
||||||
|
{_t("Loading room preview")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.Kicked: {
|
case MessageCase.Kicked: {
|
||||||
|
@ -433,7 +445,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
|
||||||
|
|
||||||
let subTitleElements;
|
let subTitleElements;
|
||||||
if (subTitle) {
|
if (subTitle) {
|
||||||
|
@ -484,6 +495,9 @@ module.exports = React.createClass({
|
||||||
{ secondaryButton }
|
{ secondaryButton }
|
||||||
{ primaryButton }
|
{ primaryButton }
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mx_RoomPreviewBar_footer">
|
||||||
|
{ footer }
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 New Vector Ltd
|
||||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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.
|
||||||
|
@ -67,14 +68,6 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_shouldShowNotifBadge: function() {
|
|
||||||
return RoomNotifs.BADGE_STATES.includes(this.state.notifState);
|
|
||||||
},
|
|
||||||
|
|
||||||
_shouldShowMentionBadge: function() {
|
|
||||||
return RoomNotifs.MENTION_BADGE_STATES.includes(this.state.notifState);
|
|
||||||
},
|
|
||||||
|
|
||||||
_isDirectMessageRoom: function(roomId) {
|
_isDirectMessageRoom: function(roomId) {
|
||||||
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||||
return Boolean(dmRooms);
|
return Boolean(dmRooms);
|
||||||
|
@ -301,8 +294,8 @@ module.exports = React.createClass({
|
||||||
const notificationCount = this.props.notificationCount;
|
const notificationCount = this.props.notificationCount;
|
||||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||||
|
|
||||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
|
const notifBadges = notificationCount > 0 && RoomNotifs.shouldShowNotifBadge(this.state.notifState);
|
||||||
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
|
const mentionBadges = this.props.highlight && RoomNotifs.shouldShowMentionBadge(this.state.notifState);
|
||||||
const badges = notifBadges || mentionBadges;
|
const badges = notifBadges || mentionBadges;
|
||||||
|
|
||||||
let subtext = null;
|
let subtext = null;
|
||||||
|
|
|
@ -97,20 +97,22 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomUpgradeWarningBar">
|
<div className="mx_RoomUpgradeWarningBar">
|
||||||
<div className="mx_RoomUpgradeWarningBar_header">
|
<div className="mx_RoomUpgradeWarningBar_wrapped">
|
||||||
{_t(
|
<div className="mx_RoomUpgradeWarningBar_header">
|
||||||
"This room is running room version <roomVersion />, which this homeserver has " +
|
{_t(
|
||||||
"marked as <i>unstable</i>.",
|
"This room is running room version <roomVersion />, which this homeserver has " +
|
||||||
{},
|
"marked as <i>unstable</i>.",
|
||||||
{
|
{},
|
||||||
"roomVersion": () => <code>{this.props.room.getVersion()}</code>,
|
{
|
||||||
"i": (sub) => <i>{sub}</i>,
|
"roomVersion": () => <code>{this.props.room.getVersion()}</code>,
|
||||||
},
|
"i": (sub) => <i>{sub}</i>,
|
||||||
)}
|
},
|
||||||
</div>
|
)}
|
||||||
{doUpgradeWarnings}
|
</div>
|
||||||
<div className="mx_RoomUpgradeWarningBar_small">
|
{doUpgradeWarnings}
|
||||||
{_t("Only room administrators will see this warning")}
|
<div className="mx_RoomUpgradeWarningBar_small">
|
||||||
|
{_t("Only room administrators will see this warning")}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import {_t, _td} from '../../../languageHandler';
|
||||||
import AppTile from '../elements/AppTile';
|
import AppTile from '../elements/AppTile';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
@ -53,6 +52,9 @@ export default class Stickerpicker extends React.Component {
|
||||||
this.popoverWidth = 300;
|
this.popoverWidth = 300;
|
||||||
this.popoverHeight = 300;
|
this.popoverHeight = 300;
|
||||||
|
|
||||||
|
// This is loaded by _acquireScalarClient on an as-needed basis.
|
||||||
|
this.scalarClient = null;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showStickers: false,
|
showStickers: false,
|
||||||
imError: null,
|
imError: null,
|
||||||
|
@ -63,14 +65,34 @@ export default class Stickerpicker extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeStickerpickerWidgets() {
|
_acquireScalarClient() {
|
||||||
|
if (this.scalarClient) return Promise.resolve(this.scalarClient);
|
||||||
|
if (ScalarAuthClient.isPossible()) {
|
||||||
|
this.scalarClient = new ScalarAuthClient();
|
||||||
|
return this.scalarClient.connect().then(() => {
|
||||||
|
this.forceUpdate();
|
||||||
|
return this.scalarClient;
|
||||||
|
}).catch((e) => {
|
||||||
|
this._imError(_td("Failed to connect to integrations server"), e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._imError(_td("No integrations server is configured to manage stickers with"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _removeStickerpickerWidgets() {
|
||||||
|
const scalarClient = await this._acquireScalarClient();
|
||||||
console.warn('Removing Stickerpicker widgets');
|
console.warn('Removing Stickerpicker widgets');
|
||||||
if (this.state.widgetId) {
|
if (this.state.widgetId) {
|
||||||
this.scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
|
if (scalarClient) {
|
||||||
console.warn('Assets disabled');
|
scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
|
||||||
}).catch((err) => {
|
console.warn('Assets disabled');
|
||||||
console.error('Failed to disable assets');
|
}).catch((err) => {
|
||||||
});
|
console.error('Failed to disable assets');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("Cannot disable assets: no scalar client");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('No widget ID specified, not disabling assets');
|
console.warn('No widget ID specified, not disabling assets');
|
||||||
}
|
}
|
||||||
|
@ -87,19 +109,7 @@ export default class Stickerpicker extends React.Component {
|
||||||
// Close the sticker picker when the window resizes
|
// Close the sticker picker when the window resizes
|
||||||
window.addEventListener('resize', this._onResize);
|
window.addEventListener('resize', this._onResize);
|
||||||
|
|
||||||
this.scalarClient = null;
|
this.dispatcherRef = dis.register(this._onWidgetAction);
|
||||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
|
||||||
this.scalarClient = new ScalarAuthClient();
|
|
||||||
this.scalarClient.connect().then(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
}).catch((e) => {
|
|
||||||
this._imError("Failed to connect to integrations server", e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.imError) {
|
|
||||||
this.dispatcherRef = dis.register(this._onWidgetAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track updates to widget state in account data
|
// Track updates to widget state in account data
|
||||||
MatrixClientPeg.get().on('accountData', this._updateWidget);
|
MatrixClientPeg.get().on('accountData', this._updateWidget);
|
||||||
|
@ -126,7 +136,7 @@ export default class Stickerpicker extends React.Component {
|
||||||
console.error(errorMsg, e);
|
console.error(errorMsg, e);
|
||||||
this.setState({
|
this.setState({
|
||||||
showStickers: false,
|
showStickers: false,
|
||||||
imError: errorMsg,
|
imError: _t(errorMsg),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,22 +349,13 @@ export default class Stickerpicker extends React.Component {
|
||||||
*/
|
*/
|
||||||
_launchManageIntegrations() {
|
_launchManageIntegrations() {
|
||||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
this.scalarClient.connect().done(() => {
|
|
||||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
// The integrations manager will handle scalar auth for us.
|
||||||
this.scalarClient.getScalarInterfaceUrlForRoom(
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
this.props.room,
|
room: this.props.room,
|
||||||
'type_' + widgetType,
|
screen: `type_${widgetType}`,
|
||||||
this.state.widgetId,
|
integrationId: this.state.widgetId,
|
||||||
) :
|
}, "mx_IntegrationsManager");
|
||||||
null;
|
|
||||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
|
||||||
src: src,
|
|
||||||
}, "mx_IntegrationsManager");
|
|
||||||
this.setState({showStickers: false});
|
|
||||||
}, (err) => {
|
|
||||||
this.setState({imError: err});
|
|
||||||
console.error('Error ensuring a valid scalar_token exists', err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
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,50 +15,124 @@ 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 PropTypes from 'prop-types';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
|
|
||||||
const React = require('react');
|
export default class IntegrationsManager extends React.Component {
|
||||||
const sdk = require('../../../index');
|
static propTypes = {
|
||||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
// the room object where the integrations manager should be opened in
|
||||||
const dis = require('../../../dispatcher');
|
room: PropTypes.object.isRequired,
|
||||||
|
|
||||||
module.exports = React.createClass({
|
// the screen name to open
|
||||||
displayName: 'IntegrationsManager',
|
screen: PropTypes.string,
|
||||||
|
|
||||||
propTypes: {
|
// the integration ID to open
|
||||||
src: React.PropTypes.string.isRequired, // the source of the integration manager being embedded
|
integrationId: PropTypes.string,
|
||||||
onFinished: React.PropTypes.func.isRequired, // callback when the lightbox is dismissed
|
|
||||||
},
|
|
||||||
|
|
||||||
// XXX: keyboard shortcuts for managing dialogs should be done by the modal
|
// callback when the manager is dismissed
|
||||||
// dialog base class somehow, surely...
|
onFinished: PropTypes.func.isRequired,
|
||||||
componentDidMount: function() {
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
configured: ScalarAuthClient.isPossible(),
|
||||||
|
connected: false, // true if a `src` is set and able to be connected to
|
||||||
|
src: null, // string for where to connect to
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
if (!this.state.configured) return;
|
||||||
|
|
||||||
|
const scalarClient = new ScalarAuthClient();
|
||||||
|
scalarClient.connect().then(() => {
|
||||||
|
const hasCredentials = scalarClient.hasCredentials();
|
||||||
|
if (!hasCredentials) {
|
||||||
|
this.setState({
|
||||||
|
connected: false,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const src = scalarClient.getScalarInterfaceUrlForRoom(
|
||||||
|
this.props.room,
|
||||||
|
this.props.screen,
|
||||||
|
this.props.integrationId,
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
connected: true,
|
||||||
|
src: src,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
connected: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
document.addEventListener("keydown", this.onKeyDown);
|
document.addEventListener("keydown", this.onKeyDown);
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
document.removeEventListener("keydown", this.onKeyDown);
|
document.removeEventListener("keydown", this.onKeyDown);
|
||||||
},
|
}
|
||||||
|
|
||||||
onKeyDown: function(ev) {
|
onKeyDown = (ev) => {
|
||||||
if (ev.keyCode == 27) { // escape
|
if (ev.keyCode === 27) { // escape
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction = (payload) => {
|
||||||
if (payload.action === 'close_scalar') {
|
if (payload.action === 'close_scalar') {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
render: function() {
|
render() {
|
||||||
return (
|
if (!this.state.configured) {
|
||||||
<iframe src={ this.props.src }></iframe>
|
return (
|
||||||
);
|
<div className='mx_IntegrationsManager_error'>
|
||||||
},
|
<h3>{_t("No integrations server configured")}</h3>
|
||||||
});
|
<p>{_t("This Riot instance does not have an integrations server configured.")}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.loading) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
return (
|
||||||
|
<div className='mx_IntegrationsManager_loading'>
|
||||||
|
<h3>{_t("Connecting to integrations server...")}</h3>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.connected) {
|
||||||
|
return (
|
||||||
|
<div className='mx_IntegrationsManager_error'>
|
||||||
|
<h3>{_t("Cannot connect to integrations server")}</h3>
|
||||||
|
<p>{_t("The integrations server is offline or it cannot reach your homeserver.")}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <iframe src={this.state.src}></iframe>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,48 +29,64 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
mediaDevices: null,
|
mediaDevices: false,
|
||||||
activeAudioOutput: null,
|
activeAudioOutput: null,
|
||||||
activeAudioInput: null,
|
activeAudioInput: null,
|
||||||
activeVideoInput: null,
|
activeVideoInput: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount(): void {
|
async componentDidMount() {
|
||||||
this._refreshMediaDevices();
|
const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
|
||||||
|
if (canSeeDeviceLabels) {
|
||||||
|
this._refreshMediaDevices();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_refreshMediaDevices = async (stream) => {
|
_refreshMediaDevices = async (stream) => {
|
||||||
if (stream) {
|
|
||||||
// kill stream so that we don't leave it lingering around with webcam enabled etc
|
|
||||||
// as here we called gUM to ask user for permission to their device names only
|
|
||||||
stream.getTracks().forEach((track) => track.stop());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
mediaDevices: await CallMediaHandler.getDevices(),
|
mediaDevices: await CallMediaHandler.getDevices(),
|
||||||
activeAudioOutput: CallMediaHandler.getAudioOutput(),
|
activeAudioOutput: CallMediaHandler.getAudioOutput(),
|
||||||
activeAudioInput: CallMediaHandler.getAudioInput(),
|
activeAudioInput: CallMediaHandler.getAudioInput(),
|
||||||
activeVideoInput: CallMediaHandler.getVideoInput(),
|
activeVideoInput: CallMediaHandler.getVideoInput(),
|
||||||
});
|
});
|
||||||
|
if (stream) {
|
||||||
|
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
|
||||||
|
// so that we don't leave it lingering around with webcam enabled etc
|
||||||
|
// as here we called gUM to ask user for permission to their device names only
|
||||||
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_requestMediaPermissions = () => {
|
_requestMediaPermissions = async () => {
|
||||||
const getUserMedia = (
|
let constraints;
|
||||||
window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia
|
let stream;
|
||||||
);
|
let error;
|
||||||
if (getUserMedia) {
|
try {
|
||||||
return getUserMedia.apply(window.navigator, [
|
constraints = {video: true, audio: true};
|
||||||
{ video: true, audio: true },
|
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
this._refreshMediaDevices,
|
} catch (err) {
|
||||||
function() {
|
// user likely doesn't have a webcam,
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
// we should still allow to select a microphone
|
||||||
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
|
if (err.name === "NotFoundError") {
|
||||||
title: _t('No media permissions'),
|
constraints = { audio: true };
|
||||||
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
|
try {
|
||||||
});
|
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
},
|
} catch (err) {
|
||||||
]);
|
error = err;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
|
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
|
||||||
|
title: _t('No media permissions'),
|
||||||
|
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._refreshMediaDevices(stream);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -100,7 +116,9 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderDeviceOptions(devices, category) {
|
_renderDeviceOptions(devices, category) {
|
||||||
return devices.map((d) => <option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
|
return devices.map((d) => {
|
||||||
|
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default class VerificationCancelled extends React.Component {
|
||||||
"The other party cancelled the verification.",
|
"The other party cancelled the verification.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t('Cancel')}
|
primaryButton={_t('OK')}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
onPrimaryButtonClick={this.props.onDone}
|
onPrimaryButtonClick={this.props.onDone}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -30,12 +30,15 @@ import {getAddressType} from "./UserAddress";
|
||||||
* @param {object=} opts parameters for creating the room
|
* @param {object=} opts parameters for creating the room
|
||||||
* @param {string=} opts.dmUserId If specified, make this a DM room for this user and invite them
|
* @param {string=} opts.dmUserId If specified, make this a DM room for this user and invite them
|
||||||
* @param {object=} opts.createOpts set of options to pass to createRoom call.
|
* @param {object=} opts.createOpts set of options to pass to createRoom call.
|
||||||
|
* @param {bool=} opts.spinner True to show a modal spinner while the room is created.
|
||||||
|
* Default: True
|
||||||
*
|
*
|
||||||
* @returns {Promise} which resolves to the room id, or null if the
|
* @returns {Promise} which resolves to the room id, or null if the
|
||||||
* action was aborted or failed.
|
* action was aborted or failed.
|
||||||
*/
|
*/
|
||||||
function createRoom(opts) {
|
function createRoom(opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
if (opts.spinner === undefined) opts.spinner = true;
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
@ -87,11 +90,12 @@ function createRoom(opts) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
let modal;
|
||||||
|
if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||||
|
|
||||||
let roomId;
|
let roomId;
|
||||||
return client.createRoom(createOpts).finally(function() {
|
return client.createRoom(createOpts).finally(function() {
|
||||||
modal.close();
|
if (modal) modal.close();
|
||||||
}).then(function(res) {
|
}).then(function(res) {
|
||||||
roomId = res.room_id;
|
roomId = res.room_id;
|
||||||
if (opts.dmUserId) {
|
if (opts.dmUserId) {
|
||||||
|
|
|
@ -15,22 +15,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {UserPillPart, RoomPillPart, PlainPart} from "./parts";
|
|
||||||
|
|
||||||
export default class AutocompleteWrapperModel {
|
export default class AutocompleteWrapperModel {
|
||||||
constructor(updateCallback, getAutocompleterComponent, updateQuery, room, client) {
|
constructor(updateCallback, getAutocompleterComponent, updateQuery, partCreator) {
|
||||||
this._updateCallback = updateCallback;
|
this._updateCallback = updateCallback;
|
||||||
this._getAutocompleterComponent = getAutocompleterComponent;
|
this._getAutocompleterComponent = getAutocompleterComponent;
|
||||||
this._updateQuery = updateQuery;
|
this._updateQuery = updateQuery;
|
||||||
|
this._partCreator = partCreator;
|
||||||
this._query = null;
|
this._query = null;
|
||||||
this._room = room;
|
|
||||||
this._client = client;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEscape(e) {
|
onEscape(e) {
|
||||||
this._getAutocompleterComponent().onEscape(e);
|
this._getAutocompleterComponent().onEscape(e);
|
||||||
this._updateCallback({
|
this._updateCallback({
|
||||||
replacePart: new PlainPart(this._queryPart.text),
|
replacePart: this._partCreator.plain(this._queryPart.text),
|
||||||
caretOffset: this._queryOffset,
|
caretOffset: this._queryOffset,
|
||||||
close: true,
|
close: true,
|
||||||
});
|
});
|
||||||
|
@ -93,21 +90,22 @@ export default class AutocompleteWrapperModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
_partForCompletion(completion) {
|
_partForCompletion(completion) {
|
||||||
const firstChr = completion.completionId && completion.completionId[0];
|
const {completionId} = completion;
|
||||||
|
const text = completion.completion;
|
||||||
|
const firstChr = completionId && completionId[0];
|
||||||
switch (firstChr) {
|
switch (firstChr) {
|
||||||
case "@": {
|
case "@": {
|
||||||
const displayName = completion.completion;
|
if (completionId === "@room") {
|
||||||
const userId = completion.completionId;
|
return this._partCreator.atRoomPill(completionId);
|
||||||
const member = this._room.getMember(userId);
|
} else {
|
||||||
return new UserPillPart(userId, displayName, member);
|
return this._partCreator.userPill(text, completionId);
|
||||||
}
|
}
|
||||||
case "#": {
|
|
||||||
const displayAlias = completion.completionId;
|
|
||||||
return new RoomPillPart(displayAlias, this._client);
|
|
||||||
}
|
}
|
||||||
|
case "#":
|
||||||
|
return this._partCreator.roomPill(completionId);
|
||||||
// also used for emoji completion
|
// also used for emoji completion
|
||||||
default:
|
default:
|
||||||
return new PlainPart(completion.completion);
|
return this._partCreator.plain(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,50 +15,104 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {needsCaretNodeBefore, needsCaretNodeAfter} from "./render";
|
||||||
|
|
||||||
export function setCaretPosition(editor, model, caretPosition) {
|
export function setCaretPosition(editor, model, caretPosition) {
|
||||||
const sel = document.getSelection();
|
const sel = document.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
|
const {offset, lineIndex, nodeIndex} = getLineAndNodePosition(model, caretPosition);
|
||||||
|
const lineNode = editor.childNodes[lineIndex];
|
||||||
|
|
||||||
|
let focusNode;
|
||||||
|
// empty line with just a <br>
|
||||||
|
if (nodeIndex === -1) {
|
||||||
|
focusNode = lineNode;
|
||||||
|
} else {
|
||||||
|
focusNode = lineNode.childNodes[nodeIndex];
|
||||||
|
// make sure we have a text node
|
||||||
|
if (focusNode.nodeType === Node.ELEMENT_NODE && focusNode.firstChild) {
|
||||||
|
focusNode = focusNode.firstChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
range.setStart(focusNode, offset);
|
||||||
|
range.collapse(true);
|
||||||
|
sel.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLineAndNodePosition(model, caretPosition) {
|
||||||
const {parts} = model;
|
const {parts} = model;
|
||||||
const {index} = caretPosition;
|
const partIndex = caretPosition.index;
|
||||||
|
const lineResult = findNodeInLineForPart(parts, partIndex);
|
||||||
|
const {lineIndex} = lineResult;
|
||||||
|
let {nodeIndex} = lineResult;
|
||||||
let {offset} = caretPosition;
|
let {offset} = caretPosition;
|
||||||
|
// we're at an empty line between a newline part
|
||||||
|
// and another newline part or end/start of parts.
|
||||||
|
// set offset to 0 so it gets set to the <br> inside the line container
|
||||||
|
if (nodeIndex === -1) {
|
||||||
|
offset = 0;
|
||||||
|
} else {
|
||||||
|
// move caret out of uneditable part (into caret node, or empty line br) if needed
|
||||||
|
({nodeIndex, offset} = moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset));
|
||||||
|
}
|
||||||
|
return {lineIndex, nodeIndex, offset};
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNodeInLineForPart(parts, partIndex) {
|
||||||
let lineIndex = 0;
|
let lineIndex = 0;
|
||||||
let nodeIndex = -1;
|
let nodeIndex = -1;
|
||||||
for (let i = 0; i <= index; ++i) {
|
|
||||||
|
let prevPart = null;
|
||||||
|
// go through to parts up till (and including) the index
|
||||||
|
// to find newline parts
|
||||||
|
for (let i = 0; i <= partIndex; ++i) {
|
||||||
const part = parts[i];
|
const part = parts[i];
|
||||||
if (part && part.type === "newline") {
|
if (part.type === "newline") {
|
||||||
if (i < index) {
|
lineIndex += 1;
|
||||||
lineIndex += 1;
|
nodeIndex = -1;
|
||||||
nodeIndex = -1;
|
prevPart = null;
|
||||||
} else {
|
} else {
|
||||||
// if index points at a newline part,
|
nodeIndex += 1;
|
||||||
// put the caret at the end of the previous part
|
if (needsCaretNodeBefore(part, prevPart)) {
|
||||||
// so it stays on the same line
|
nodeIndex += 1;
|
||||||
const prevPart = parts[i - 1];
|
}
|
||||||
offset = prevPart ? prevPart.text.length : 0;
|
// only jump over caret node if we're not at our destination node already,
|
||||||
|
// as we'll assume in moveOutOfUneditablePart that nodeIndex
|
||||||
|
// refers to the node corresponding to the part,
|
||||||
|
// and not an adjacent caret node
|
||||||
|
if (i < partIndex) {
|
||||||
|
const nextPart = parts[i + 1];
|
||||||
|
const isLastOfLine = !nextPart || nextPart.type === "newline";
|
||||||
|
if (needsCaretNodeAfter(part, isLastOfLine)) {
|
||||||
|
nodeIndex += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevPart = part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {lineIndex, nodeIndex};
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset) {
|
||||||
|
// move caret before or after uneditable part
|
||||||
|
const part = parts[partIndex];
|
||||||
|
if (part && !part.canEdit) {
|
||||||
|
if (offset === 0) {
|
||||||
|
nodeIndex -= 1;
|
||||||
|
const prevPart = parts[partIndex - 1];
|
||||||
|
// if the previous node is a caret node, it's empty
|
||||||
|
// so the offset can stay at 0
|
||||||
|
// only when it's not, we need to set the offset
|
||||||
|
// at the end of the node
|
||||||
|
if (!needsCaretNodeBefore(part, prevPart)) {
|
||||||
|
offset = prevPart.text.length;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nodeIndex += 1;
|
nodeIndex += 1;
|
||||||
|
offset = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let focusNode;
|
return {nodeIndex, offset};
|
||||||
const lineNode = editor.childNodes[lineIndex];
|
|
||||||
if (lineNode) {
|
|
||||||
focusNode = lineNode.childNodes[nodeIndex];
|
|
||||||
if (!focusNode) {
|
|
||||||
focusNode = lineNode;
|
|
||||||
} else if (focusNode.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
focusNode = focusNode.childNodes[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// node not found, set caret at end
|
|
||||||
if (!focusNode) {
|
|
||||||
range.selectNodeContents(editor);
|
|
||||||
range.collapse(false);
|
|
||||||
} else {
|
|
||||||
// make sure we have a text node
|
|
||||||
range.setStart(focusNode, offset);
|
|
||||||
range.collapse(true);
|
|
||||||
}
|
|
||||||
sel.addRange(range);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,73 +16,92 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
|
import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
|
||||||
import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts";
|
|
||||||
import { walkDOMDepthFirst } from "./dom";
|
import { walkDOMDepthFirst } from "./dom";
|
||||||
|
|
||||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||||
|
|
||||||
function parseLink(a, room, client) {
|
function parseAtRoomMentions(text, partCreator) {
|
||||||
|
const ATROOM = "@room";
|
||||||
|
const parts = [];
|
||||||
|
text.split(ATROOM).forEach((textPart, i, arr) => {
|
||||||
|
if (textPart.length) {
|
||||||
|
parts.push(partCreator.plain(textPart));
|
||||||
|
}
|
||||||
|
// it's safe to never append @room after the last textPart
|
||||||
|
// as split will report an empty string at the end if
|
||||||
|
// `text` ended in @room.
|
||||||
|
const isLast = i === arr.length - 1;
|
||||||
|
if (!isLast) {
|
||||||
|
parts.push(partCreator.atRoomPill(ATROOM));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLink(a, partCreator) {
|
||||||
const {href} = a;
|
const {href} = a;
|
||||||
const pillMatch = REGEX_MATRIXTO.exec(href) || [];
|
const pillMatch = REGEX_MATRIXTO.exec(href) || [];
|
||||||
const resourceId = pillMatch[1]; // The room/user ID
|
const resourceId = pillMatch[1]; // The room/user ID
|
||||||
const prefix = pillMatch[2]; // The first character of prefix
|
const prefix = pillMatch[2]; // The first character of prefix
|
||||||
switch (prefix) {
|
switch (prefix) {
|
||||||
case "@":
|
case "@":
|
||||||
return new UserPillPart(
|
return partCreator.userPill(a.textContent, resourceId);
|
||||||
resourceId,
|
|
||||||
a.textContent,
|
|
||||||
room.getMember(resourceId),
|
|
||||||
);
|
|
||||||
case "#":
|
case "#":
|
||||||
return new RoomPillPart(resourceId, client);
|
return partCreator.roomPill(resourceId);
|
||||||
default: {
|
default: {
|
||||||
if (href === a.textContent) {
|
if (href === a.textContent) {
|
||||||
return new PlainPart(a.textContent);
|
return partCreator.plain(a.textContent);
|
||||||
} else {
|
} else {
|
||||||
return new PlainPart(`[${a.textContent}](${href})`);
|
return partCreator.plain(`[${a.textContent}](${href})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCodeBlock(n) {
|
function parseCodeBlock(n, partCreator) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
const preLines = ("```\n" + n.textContent + "```").split("\n");
|
const preLines = ("```\n" + n.textContent + "```").split("\n");
|
||||||
preLines.forEach((l, i) => {
|
preLines.forEach((l, i) => {
|
||||||
parts.push(new PlainPart(l));
|
parts.push(partCreator.plain(l));
|
||||||
if (i < preLines.length - 1) {
|
if (i < preLines.length - 1) {
|
||||||
parts.push(new NewlinePart("\n"));
|
parts.push(partCreator.newline());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseElement(n, room, client) {
|
function parseElement(n, partCreator, state) {
|
||||||
switch (n.nodeName) {
|
switch (n.nodeName) {
|
||||||
case "A":
|
case "A":
|
||||||
return parseLink(n, room, client);
|
return parseLink(n, partCreator);
|
||||||
case "BR":
|
case "BR":
|
||||||
return new NewlinePart("\n");
|
return partCreator.newline();
|
||||||
case "EM":
|
case "EM":
|
||||||
return new PlainPart(`*${n.textContent}*`);
|
return partCreator.plain(`*${n.textContent}*`);
|
||||||
case "STRONG":
|
case "STRONG":
|
||||||
return new PlainPart(`**${n.textContent}**`);
|
return partCreator.plain(`**${n.textContent}**`);
|
||||||
case "PRE":
|
case "PRE":
|
||||||
return parseCodeBlock(n);
|
return parseCodeBlock(n, partCreator);
|
||||||
case "CODE":
|
case "CODE":
|
||||||
return new PlainPart(`\`${n.textContent}\``);
|
return partCreator.plain(`\`${n.textContent}\``);
|
||||||
case "DEL":
|
case "DEL":
|
||||||
return new PlainPart(`<del>${n.textContent}</del>`);
|
return partCreator.plain(`<del>${n.textContent}</del>`);
|
||||||
case "LI":
|
case "LI": {
|
||||||
|
const indent = " ".repeat(state.listDepth - 1);
|
||||||
if (n.parentElement.nodeName === "OL") {
|
if (n.parentElement.nodeName === "OL") {
|
||||||
return new PlainPart(` 1. `);
|
return partCreator.plain(`${indent}1. `);
|
||||||
} else {
|
} else {
|
||||||
return new PlainPart(` - `);
|
return partCreator.plain(`${indent}- `);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
case "OL":
|
||||||
|
case "UL":
|
||||||
|
state.listDepth = (state.listDepth || 0) + 1;
|
||||||
|
// es-lint-disable-next-line no-fallthrough
|
||||||
default:
|
default:
|
||||||
// don't textify block nodes we'll decend into
|
// don't textify block nodes we'll decend into
|
||||||
if (!checkDecendInto(n)) {
|
if (!checkDecendInto(n)) {
|
||||||
return new PlainPart(n.textContent);
|
return partCreator.plain(n.textContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,22 +144,22 @@ function checkIgnored(n) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function prefixQuoteLines(isFirstNode, parts) {
|
function prefixQuoteLines(isFirstNode, parts, partCreator) {
|
||||||
const PREFIX = "> ";
|
const PREFIX = "> ";
|
||||||
// a newline (to append a > to) wouldn't be added to parts for the first line
|
// a newline (to append a > to) wouldn't be added to parts for the first line
|
||||||
// if there was no content before the BLOCKQUOTE, so handle that
|
// if there was no content before the BLOCKQUOTE, so handle that
|
||||||
if (isFirstNode) {
|
if (isFirstNode) {
|
||||||
parts.splice(0, 0, new PlainPart(PREFIX));
|
parts.splice(0, 0, partCreator.plain(PREFIX));
|
||||||
}
|
}
|
||||||
for (let i = 0; i < parts.length; i += 1) {
|
for (let i = 0; i < parts.length; i += 1) {
|
||||||
if (parts[i].type === "newline") {
|
if (parts[i].type === "newline") {
|
||||||
parts.splice(i + 1, 0, new PlainPart(PREFIX));
|
parts.splice(i + 1, 0, partCreator.plain(PREFIX));
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseHtmlMessage(html, room, client) {
|
function parseHtmlMessage(html, partCreator) {
|
||||||
// no nodes from parsing here should be inserted in the document,
|
// no nodes from parsing here should be inserted in the document,
|
||||||
// as scripts in event handlers, etc would be executed then.
|
// as scripts in event handlers, etc would be executed then.
|
||||||
// we're only taking text, so that is fine
|
// we're only taking text, so that is fine
|
||||||
|
@ -148,6 +167,7 @@ function parseHtmlMessage(html, room, client) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
let lastNode;
|
let lastNode;
|
||||||
let inQuote = false;
|
let inQuote = false;
|
||||||
|
const state = {};
|
||||||
|
|
||||||
function onNodeEnter(n) {
|
function onNodeEnter(n) {
|
||||||
if (checkIgnored(n)) {
|
if (checkIgnored(n)) {
|
||||||
|
@ -159,13 +179,13 @@ function parseHtmlMessage(html, room, client) {
|
||||||
|
|
||||||
const newParts = [];
|
const newParts = [];
|
||||||
if (lastNode && (checkBlockNode(lastNode) || checkBlockNode(n))) {
|
if (lastNode && (checkBlockNode(lastNode) || checkBlockNode(n))) {
|
||||||
newParts.push(new NewlinePart("\n"));
|
newParts.push(partCreator.newline());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (n.nodeType === Node.TEXT_NODE) {
|
if (n.nodeType === Node.TEXT_NODE) {
|
||||||
newParts.push(new PlainPart(n.nodeValue));
|
newParts.push(...parseAtRoomMentions(n.nodeValue, partCreator));
|
||||||
} else if (n.nodeType === Node.ELEMENT_NODE) {
|
} else if (n.nodeType === Node.ELEMENT_NODE) {
|
||||||
const parseResult = parseElement(n, room, client);
|
const parseResult = parseElement(n, partCreator, state);
|
||||||
if (parseResult) {
|
if (parseResult) {
|
||||||
if (Array.isArray(parseResult)) {
|
if (Array.isArray(parseResult)) {
|
||||||
newParts.push(...parseResult);
|
newParts.push(...parseResult);
|
||||||
|
@ -177,14 +197,14 @@ function parseHtmlMessage(html, room, client) {
|
||||||
|
|
||||||
if (newParts.length && inQuote) {
|
if (newParts.length && inQuote) {
|
||||||
const isFirstPart = parts.length === 0;
|
const isFirstPart = parts.length === 0;
|
||||||
prefixQuoteLines(isFirstPart, newParts);
|
prefixQuoteLines(isFirstPart, newParts, partCreator);
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push(...newParts);
|
parts.push(...newParts);
|
||||||
|
|
||||||
// extra newline after quote, only if there something behind it...
|
// extra newline after quote, only if there something behind it...
|
||||||
if (lastNode && lastNode.nodeName === "BLOCKQUOTE") {
|
if (lastNode && lastNode.nodeName === "BLOCKQUOTE") {
|
||||||
parts.push(new NewlinePart("\n"));
|
parts.push(partCreator.newline());
|
||||||
}
|
}
|
||||||
lastNode = null;
|
lastNode = null;
|
||||||
return checkDecendInto(n);
|
return checkDecendInto(n);
|
||||||
|
@ -194,8 +214,14 @@ function parseHtmlMessage(html, room, client) {
|
||||||
if (checkIgnored(n)) {
|
if (checkIgnored(n)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (n.nodeName === "BLOCKQUOTE") {
|
switch (n.nodeName) {
|
||||||
inQuote = false;
|
case "BLOCKQUOTE":
|
||||||
|
inQuote = false;
|
||||||
|
break;
|
||||||
|
case "OL":
|
||||||
|
case "UL":
|
||||||
|
state.listDepth -= 1;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
lastNode = n;
|
lastNode = n;
|
||||||
}
|
}
|
||||||
|
@ -205,23 +231,25 @@ function parseHtmlMessage(html, room, client) {
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseEvent(event, room, client) {
|
export function parseEvent(event, partCreator) {
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
|
let parts;
|
||||||
if (content.format === "org.matrix.custom.html") {
|
if (content.format === "org.matrix.custom.html") {
|
||||||
return parseHtmlMessage(content.formatted_body || "", room, client);
|
parts = parseHtmlMessage(content.formatted_body || "", partCreator);
|
||||||
} else {
|
} else {
|
||||||
const body = content.body || "";
|
const body = content.body || "";
|
||||||
const lines = body.split("\n");
|
const lines = body.split("\n");
|
||||||
const parts = lines.reduce((parts, line, i) => {
|
parts = lines.reduce((parts, line, i) => {
|
||||||
const isLast = i === lines.length - 1;
|
const isLast = i === lines.length - 1;
|
||||||
const text = new PlainPart(line);
|
const newParts = parseAtRoomMentions(line, partCreator);
|
||||||
const newLine = !isLast && new NewlinePart("\n");
|
if (!isLast) {
|
||||||
if (newLine) {
|
newParts.push(partCreator.newline());
|
||||||
return parts.concat(text, newLine);
|
|
||||||
} else {
|
|
||||||
return parts.concat(text);
|
|
||||||
}
|
}
|
||||||
|
return parts.concat(newParts);
|
||||||
}, []);
|
}, []);
|
||||||
return parts;
|
|
||||||
}
|
}
|
||||||
|
if (content.msgtype === "m.emote") {
|
||||||
|
parts.unshift(partCreator.plain("/me "));
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {CARET_NODE_CHAR, isCaretNode} from "./render";
|
||||||
|
|
||||||
export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) {
|
export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) {
|
||||||
let node = rootNode.firstChild;
|
let node = rootNode.firstChild;
|
||||||
while (node && node !== rootNode) {
|
while (node && node !== rootNode) {
|
||||||
|
@ -38,27 +40,54 @@ export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCaretOffsetAndText(editor, sel) {
|
export function getCaretOffsetAndText(editor, sel) {
|
||||||
let {focusNode} = sel;
|
let {focusNode, focusOffset} = sel;
|
||||||
const {focusOffset} = sel;
|
// sometimes focusNode is an element, and then focusOffset means
|
||||||
let caretOffset = focusOffset;
|
// the index of a child element ... - 1 🤷
|
||||||
|
if (focusNode.nodeType === Node.ELEMENT_NODE && focusOffset !== 0) {
|
||||||
|
focusNode = focusNode.childNodes[focusOffset - 1];
|
||||||
|
focusOffset = focusNode.textContent.length;
|
||||||
|
}
|
||||||
|
const {text, focusNodeOffset} = getTextAndFocusNodeOffset(editor, focusNode, focusOffset);
|
||||||
|
const caret = getCaret(focusNode, focusNodeOffset, focusOffset);
|
||||||
|
return {caret, text};
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets the caret position details, ignoring and adjusting to
|
||||||
|
// the ZWS if you're typing in a caret node
|
||||||
|
function getCaret(focusNode, focusNodeOffset, focusOffset) {
|
||||||
|
let atNodeEnd = focusOffset === focusNode.textContent.length;
|
||||||
|
if (focusNode.nodeType === Node.TEXT_NODE && isCaretNode(focusNode.parentElement)) {
|
||||||
|
const zwsIdx = focusNode.nodeValue.indexOf(CARET_NODE_CHAR);
|
||||||
|
if (zwsIdx !== -1 && zwsIdx < focusOffset) {
|
||||||
|
focusOffset -= 1;
|
||||||
|
}
|
||||||
|
// if typing in a caret node, you're either typing before or after the ZWS.
|
||||||
|
// In both cases, you should be considered at node end because the ZWS is
|
||||||
|
// not included in the text here, and once the model is updated and rerendered,
|
||||||
|
// that caret node will be removed.
|
||||||
|
atNodeEnd = true;
|
||||||
|
}
|
||||||
|
return {offset: focusNodeOffset + focusOffset, atNodeEnd};
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets the text of the editor as a string,
|
||||||
|
// and the offset in characters where the focusNode starts in that string
|
||||||
|
// all ZWS from caret nodes are filtered out
|
||||||
|
function getTextAndFocusNodeOffset(editor, focusNode, focusOffset) {
|
||||||
|
let focusNodeOffset = 0;
|
||||||
let foundCaret = false;
|
let foundCaret = false;
|
||||||
let text = "";
|
let text = "";
|
||||||
|
|
||||||
if (focusNode.nodeType === Node.ELEMENT_NODE && focusOffset !== 0) {
|
|
||||||
focusNode = focusNode.childNodes[focusOffset - 1];
|
|
||||||
caretOffset = focusNode.textContent.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function enterNodeCallback(node) {
|
function enterNodeCallback(node) {
|
||||||
const nodeText = node.nodeType === Node.TEXT_NODE && node.nodeValue;
|
|
||||||
if (!foundCaret) {
|
if (!foundCaret) {
|
||||||
if (node === focusNode) {
|
if (node === focusNode) {
|
||||||
foundCaret = true;
|
foundCaret = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const nodeText = node.nodeType === Node.TEXT_NODE && getTextNodeValue(node);
|
||||||
if (nodeText) {
|
if (nodeText) {
|
||||||
if (!foundCaret) {
|
if (!foundCaret) {
|
||||||
caretOffset += nodeText.length;
|
focusNodeOffset += nodeText.length;
|
||||||
}
|
}
|
||||||
text += nodeText;
|
text += nodeText;
|
||||||
}
|
}
|
||||||
|
@ -73,14 +102,30 @@ export function getCaretOffsetAndText(editor, sel) {
|
||||||
if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") {
|
if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") {
|
||||||
text += "\n";
|
text += "\n";
|
||||||
if (!foundCaret) {
|
if (!foundCaret) {
|
||||||
caretOffset += 1;
|
focusNodeOffset += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback);
|
walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback);
|
||||||
|
|
||||||
const atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length;
|
return {text, focusNodeOffset};
|
||||||
const caret = {atNodeEnd, offset: caretOffset};
|
}
|
||||||
return {caret, text};
|
|
||||||
|
// get text value of text node, ignoring ZWS if it's a caret node
|
||||||
|
function getTextNodeValue(node) {
|
||||||
|
const nodeText = node.nodeValue;
|
||||||
|
// filter out ZWS for caret nodes
|
||||||
|
if (isCaretNode(node.parentElement)) {
|
||||||
|
// typed in the caret node, so there is now something more in it than the ZWS
|
||||||
|
// so filter out the ZWS, and take the typed text into account
|
||||||
|
if (nodeText.length !== 1) {
|
||||||
|
return nodeText.replace(CARET_NODE_CHAR, "");
|
||||||
|
} else {
|
||||||
|
// only contains ZWS, which is ignored, so return emtpy string
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nodeText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,10 @@ export default class EditorModel {
|
||||||
this._updateCallback = updateCallback;
|
this._updateCallback = updateCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new EditorModel(this._parts, this._partCreator, this._updateCallback);
|
||||||
|
}
|
||||||
|
|
||||||
_insertPart(index, part) {
|
_insertPart(index, part) {
|
||||||
this._parts.splice(index, 0, part);
|
this._parts.splice(index, 0, part);
|
||||||
if (this._activePartIdx >= index) {
|
if (this._activePartIdx >= index) {
|
||||||
|
@ -91,7 +95,7 @@ export default class EditorModel {
|
||||||
const position = this.positionForOffset(diff.at, caret.atNodeEnd);
|
const position = this.positionForOffset(diff.at, caret.atNodeEnd);
|
||||||
let removedOffsetDecrease = 0;
|
let removedOffsetDecrease = 0;
|
||||||
if (diff.removed) {
|
if (diff.removed) {
|
||||||
removedOffsetDecrease = this._removeText(position, diff.removed.length);
|
removedOffsetDecrease = this.removeText(position, diff.removed.length);
|
||||||
}
|
}
|
||||||
let addedLen = 0;
|
let addedLen = 0;
|
||||||
if (diff.added) {
|
if (diff.added) {
|
||||||
|
@ -99,8 +103,7 @@ export default class EditorModel {
|
||||||
}
|
}
|
||||||
this._mergeAdjacentParts();
|
this._mergeAdjacentParts();
|
||||||
const caretOffset = diff.at - removedOffsetDecrease + addedLen;
|
const caretOffset = diff.at - removedOffsetDecrease + addedLen;
|
||||||
let newPosition = this.positionForOffset(caretOffset, true);
|
const newPosition = this.positionForOffset(caretOffset, true);
|
||||||
newPosition = newPosition.skipUneditableParts(this._parts);
|
|
||||||
this._setActivePart(newPosition);
|
this._setActivePart(newPosition);
|
||||||
this._updateCallback(newPosition);
|
this._updateCallback(newPosition);
|
||||||
}
|
}
|
||||||
|
@ -136,10 +139,9 @@ export default class EditorModel {
|
||||||
let pos;
|
let pos;
|
||||||
if (replacePart) {
|
if (replacePart) {
|
||||||
this._replacePart(this._autoCompletePartIdx, replacePart);
|
this._replacePart(this._autoCompletePartIdx, replacePart);
|
||||||
let index = this._autoCompletePartIdx;
|
const index = this._autoCompletePartIdx;
|
||||||
if (caretOffset === undefined) {
|
if (caretOffset === undefined) {
|
||||||
caretOffset = 0;
|
caretOffset = replacePart.text.length;
|
||||||
index += 1;
|
|
||||||
}
|
}
|
||||||
pos = new DocumentPosition(index, caretOffset);
|
pos = new DocumentPosition(index, caretOffset);
|
||||||
}
|
}
|
||||||
|
@ -154,11 +156,11 @@ export default class EditorModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
_mergeAdjacentParts(docPos) {
|
_mergeAdjacentParts(docPos) {
|
||||||
let prevPart = this._parts[0];
|
let prevPart;
|
||||||
for (let i = 1; i < this._parts.length; ++i) {
|
for (let i = 0; i < this._parts.length; ++i) {
|
||||||
let part = this._parts[i];
|
let part = this._parts[i];
|
||||||
const isEmpty = !part.text.length;
|
const isEmpty = !part.text.length;
|
||||||
const isMerged = !isEmpty && prevPart.merge(part);
|
const isMerged = !isEmpty && prevPart && prevPart.merge(part);
|
||||||
if (isEmpty || isMerged) {
|
if (isEmpty || isMerged) {
|
||||||
// remove empty or merged part
|
// remove empty or merged part
|
||||||
part = prevPart;
|
part = prevPart;
|
||||||
|
@ -177,7 +179,7 @@ export default class EditorModel {
|
||||||
* @return {Number} how many characters before pos were also removed,
|
* @return {Number} how many characters before pos were also removed,
|
||||||
* usually because of non-editable parts that can only be removed in their entirety.
|
* usually because of non-editable parts that can only be removed in their entirety.
|
||||||
*/
|
*/
|
||||||
_removeText(pos, len) {
|
removeText(pos, len) {
|
||||||
let {index, offset} = pos;
|
let {index, offset} = pos;
|
||||||
let removedOffsetDecrease = 0;
|
let removedOffsetDecrease = 0;
|
||||||
while (len > 0) {
|
while (len > 0) {
|
||||||
|
@ -279,13 +281,4 @@ class DocumentPosition {
|
||||||
get offset() {
|
get offset() {
|
||||||
return this._offset;
|
return this._offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
skipUneditableParts(parts) {
|
|
||||||
const part = parts[this.index];
|
|
||||||
if (part && !part.canEdit) {
|
|
||||||
return new DocumentPosition(this.index + 1, 0);
|
|
||||||
} else {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ class BasePart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PlainPart extends BasePart {
|
class PlainPart extends BasePart {
|
||||||
acceptsInsertion(chr) {
|
acceptsInsertion(chr) {
|
||||||
return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n";
|
return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n";
|
||||||
}
|
}
|
||||||
|
@ -199,7 +199,7 @@ class PillPart extends BasePart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NewlinePart extends BasePart {
|
class NewlinePart extends BasePart {
|
||||||
acceptsInsertion(chr, i) {
|
acceptsInsertion(chr, i) {
|
||||||
return (this.text.length + i) === 0 && chr === "\n";
|
return (this.text.length + i) === 0 && chr === "\n";
|
||||||
}
|
}
|
||||||
|
@ -235,20 +235,10 @@ export class NewlinePart extends BasePart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RoomPillPart extends PillPart {
|
class RoomPillPart extends PillPart {
|
||||||
constructor(displayAlias, client) {
|
constructor(displayAlias, room) {
|
||||||
super(displayAlias, displayAlias);
|
super(displayAlias, displayAlias);
|
||||||
this._room = this._findRoomByAlias(displayAlias, client);
|
this._room = room;
|
||||||
}
|
|
||||||
|
|
||||||
_findRoomByAlias(alias, client) {
|
|
||||||
if (alias[0] === '#') {
|
|
||||||
return client.getRooms().find((r) => {
|
|
||||||
return r.getAliases().includes(alias);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return client.getRoom(alias);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setAvatar(node) {
|
setAvatar(node) {
|
||||||
|
@ -270,7 +260,13 @@ export class RoomPillPart extends PillPart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserPillPart extends PillPart {
|
class AtRoomPillPart extends RoomPillPart {
|
||||||
|
get type() {
|
||||||
|
return "at-room-pill";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserPillPart extends PillPart {
|
||||||
constructor(userId, displayName, member) {
|
constructor(userId, displayName, member) {
|
||||||
super(userId, displayName);
|
super(userId, displayName);
|
||||||
this._member = member;
|
this._member = member;
|
||||||
|
@ -311,7 +307,7 @@ export class UserPillPart extends PillPart {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class PillCandidatePart extends PlainPart {
|
class PillCandidatePart extends PlainPart {
|
||||||
constructor(text, autoCompleteCreator) {
|
constructor(text, autoCompleteCreator) {
|
||||||
super(text);
|
super(text);
|
||||||
this._autoCompleteCreator = autoCompleteCreator;
|
this._autoCompleteCreator = autoCompleteCreator;
|
||||||
|
@ -351,8 +347,7 @@ export class PartCreator {
|
||||||
updateCallback,
|
updateCallback,
|
||||||
getAutocompleterComponent,
|
getAutocompleterComponent,
|
||||||
updateQuery,
|
updateQuery,
|
||||||
room,
|
this,
|
||||||
client,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -362,7 +357,7 @@ export class PartCreator {
|
||||||
case "#":
|
case "#":
|
||||||
case "@":
|
case "@":
|
||||||
case ":":
|
case ":":
|
||||||
return new PillCandidatePart("", this._autoCompleteCreator);
|
return this.pillCandidate("");
|
||||||
case "\n":
|
case "\n":
|
||||||
return new NewlinePart();
|
return new NewlinePart();
|
||||||
default:
|
default:
|
||||||
|
@ -371,24 +366,57 @@ export class PartCreator {
|
||||||
}
|
}
|
||||||
|
|
||||||
createDefaultPart(text) {
|
createDefaultPart(text) {
|
||||||
return new PlainPart(text);
|
return this.plain(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializePart(part) {
|
deserializePart(part) {
|
||||||
switch (part.type) {
|
switch (part.type) {
|
||||||
case "plain":
|
case "plain":
|
||||||
return new PlainPart(part.text);
|
return this.plain(part.text);
|
||||||
case "newline":
|
case "newline":
|
||||||
return new NewlinePart(part.text);
|
return this.newline();
|
||||||
|
case "at-room-pill":
|
||||||
|
return this.atRoomPill(part.text);
|
||||||
case "pill-candidate":
|
case "pill-candidate":
|
||||||
return new PillCandidatePart(part.text, this._autoCompleteCreator);
|
return this.pillCandidate(part.text);
|
||||||
case "room-pill":
|
case "room-pill":
|
||||||
return new RoomPillPart(part.text, this._client);
|
return this.roomPill(part.text);
|
||||||
case "user-pill": {
|
case "user-pill":
|
||||||
const member = this._room.getMember(part.userId);
|
return this.userPill(part.text, part.userId);
|
||||||
return new UserPillPart(part.userId, part.text, member);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plain(text) {
|
||||||
|
return new PlainPart(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
newline() {
|
||||||
|
return new NewlinePart("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
pillCandidate(text) {
|
||||||
|
return new PillCandidatePart(text, this._autoCompleteCreator);
|
||||||
|
}
|
||||||
|
|
||||||
|
roomPill(alias) {
|
||||||
|
let room;
|
||||||
|
if (alias[0] === '#') {
|
||||||
|
room = this._client.getRooms().find((r) => {
|
||||||
|
return r.getAliases().includes(alias);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
room = this._client.getRoom(alias);
|
||||||
|
}
|
||||||
|
return new RoomPillPart(alias, room);
|
||||||
|
}
|
||||||
|
|
||||||
|
atRoomPill(text) {
|
||||||
|
return new AtRoomPillPart(text, this._room);
|
||||||
|
}
|
||||||
|
|
||||||
|
userPill(displayName, userId) {
|
||||||
|
const member = this._room.getMember(userId);
|
||||||
|
return new UserPillPart(userId, displayName, member);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,137 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export function needsCaretNodeBefore(part, prevPart) {
|
||||||
|
const isFirst = !prevPart || prevPart.type === "newline";
|
||||||
|
return !part.canEdit && (isFirst || !prevPart.canEdit);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needsCaretNodeAfter(part, isLastOfLine) {
|
||||||
|
return !part.canEdit && isLastOfLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertAfter(node, nodeToInsert) {
|
||||||
|
const next = node.nextSibling;
|
||||||
|
if (next) {
|
||||||
|
node.parentElement.insertBefore(nodeToInsert, next);
|
||||||
|
} else {
|
||||||
|
node.parentElement.appendChild(nodeToInsert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a BOM marker for caret nodes.
|
||||||
|
// On a first test, they seem to be filtered out when copying text out of the editor,
|
||||||
|
// but this could be platform dependent.
|
||||||
|
// As a precautionary measure, I chose the character that slate also uses.
|
||||||
|
export const CARET_NODE_CHAR = "\ufeff";
|
||||||
|
// a caret node is a node that allows the caret to be placed
|
||||||
|
// where otherwise it wouldn't be possible
|
||||||
|
// (e.g. next to a pill span without adjacent text node)
|
||||||
|
function createCaretNode() {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "caretNode";
|
||||||
|
span.appendChild(document.createTextNode(CARET_NODE_CHAR));
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCaretNode(node) {
|
||||||
|
// ensure the caret node contains only a zero-width space
|
||||||
|
if (node.textContent !== CARET_NODE_CHAR) {
|
||||||
|
node.textContent = CARET_NODE_CHAR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCaretNode(node) {
|
||||||
|
return node && node.tagName === "SPAN" && node.className === "caretNode";
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNextSiblings(node) {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node = node.nextSibling;
|
||||||
|
while (node) {
|
||||||
|
const removeNode = node;
|
||||||
|
node = node.nextSibling;
|
||||||
|
removeNode.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeChildren(parent) {
|
||||||
|
const firstChild = parent.firstChild;
|
||||||
|
if (firstChild) {
|
||||||
|
removeNextSiblings(firstChild);
|
||||||
|
firstChild.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileLine(lineContainer, parts) {
|
||||||
|
let currentNode;
|
||||||
|
let prevPart;
|
||||||
|
const lastPart = parts[parts.length - 1];
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const isFirst = !prevPart;
|
||||||
|
currentNode = isFirst ? lineContainer.firstChild : currentNode.nextSibling;
|
||||||
|
|
||||||
|
if (needsCaretNodeBefore(part, prevPart)) {
|
||||||
|
if (isCaretNode(currentNode)) {
|
||||||
|
updateCaretNode(currentNode);
|
||||||
|
currentNode = currentNode.nextSibling;
|
||||||
|
} else {
|
||||||
|
lineContainer.insertBefore(createCaretNode(), currentNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove nodes until matching current part
|
||||||
|
while (currentNode && !part.canUpdateDOMNode(currentNode)) {
|
||||||
|
const nextNode = currentNode.nextSibling;
|
||||||
|
lineContainer.removeChild(currentNode);
|
||||||
|
currentNode = nextNode;
|
||||||
|
}
|
||||||
|
// update or insert node for current part
|
||||||
|
if (currentNode && part) {
|
||||||
|
part.updateDOMNode(currentNode);
|
||||||
|
} else if (part) {
|
||||||
|
currentNode = part.toDOMNode();
|
||||||
|
// hooks up nextSibling for next iteration
|
||||||
|
lineContainer.appendChild(currentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsCaretNodeAfter(part, part === lastPart)) {
|
||||||
|
if (isCaretNode(currentNode.nextSibling)) {
|
||||||
|
currentNode = currentNode.nextSibling;
|
||||||
|
updateCaretNode(currentNode);
|
||||||
|
} else {
|
||||||
|
const caretNode = createCaretNode();
|
||||||
|
insertAfter(currentNode, caretNode);
|
||||||
|
currentNode = caretNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPart = part;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeNextSiblings(currentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileEmptyLine(lineContainer) {
|
||||||
|
// empty div needs to have a BR in it to give it height
|
||||||
|
let foundBR = false;
|
||||||
|
let partNode = lineContainer.firstChild;
|
||||||
|
while (partNode) {
|
||||||
|
const nextNode = partNode.nextSibling;
|
||||||
|
if (!foundBR && partNode.tagName === "BR") {
|
||||||
|
foundBR = true;
|
||||||
|
} else {
|
||||||
|
partNode.remove();
|
||||||
|
}
|
||||||
|
partNode = nextNode;
|
||||||
|
}
|
||||||
|
if (!foundBR) {
|
||||||
|
lineContainer.appendChild(document.createElement("br"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function renderModel(editor, model) {
|
export function renderModel(editor, model) {
|
||||||
const lines = model.parts.reduce((lines, part) => {
|
const lines = model.parts.reduce((lines, part) => {
|
||||||
if (part.type === "newline") {
|
if (part.type === "newline") {
|
||||||
|
@ -25,8 +156,9 @@ export function renderModel(editor, model) {
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}, [[]]);
|
}, [[]]);
|
||||||
// TODO: refactor this code, DRY it
|
|
||||||
lines.forEach((parts, i) => {
|
lines.forEach((parts, i) => {
|
||||||
|
// find first (and remove anything else) div without className
|
||||||
|
// (as browsers insert these in contenteditable) line container
|
||||||
let lineContainer = editor.childNodes[i];
|
let lineContainer = editor.childNodes[i];
|
||||||
while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) {
|
while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) {
|
||||||
editor.removeChild(lineContainer);
|
editor.removeChild(lineContainer);
|
||||||
|
@ -38,46 +170,14 @@ export function renderModel(editor, model) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parts.length) {
|
if (parts.length) {
|
||||||
parts.forEach((part, j) => {
|
reconcileLine(lineContainer, parts);
|
||||||
let partNode = lineContainer.childNodes[j];
|
|
||||||
while (partNode && !part.canUpdateDOMNode(partNode)) {
|
|
||||||
lineContainer.removeChild(partNode);
|
|
||||||
partNode = lineContainer.childNodes[j];
|
|
||||||
}
|
|
||||||
if (partNode && part) {
|
|
||||||
part.updateDOMNode(partNode);
|
|
||||||
} else if (part) {
|
|
||||||
lineContainer.appendChild(part.toDOMNode());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let surplusElementCount = Math.max(0, lineContainer.childNodes.length - parts.length);
|
|
||||||
while (surplusElementCount) {
|
|
||||||
lineContainer.removeChild(lineContainer.lastChild);
|
|
||||||
--surplusElementCount;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// empty div needs to have a BR in it to give it height
|
reconcileEmptyLine(lineContainer);
|
||||||
let foundBR = false;
|
|
||||||
let partNode = lineContainer.firstChild;
|
|
||||||
while (partNode) {
|
|
||||||
const nextNode = partNode.nextSibling;
|
|
||||||
if (!foundBR && partNode.tagName === "BR") {
|
|
||||||
foundBR = true;
|
|
||||||
} else {
|
|
||||||
lineContainer.removeChild(partNode);
|
|
||||||
}
|
|
||||||
partNode = nextNode;
|
|
||||||
}
|
|
||||||
if (!foundBR) {
|
|
||||||
lineContainer.appendChild(document.createElement("br"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let surplusElementCount = Math.max(0, editor.childNodes.length - lines.length);
|
|
||||||
while (surplusElementCount) {
|
|
||||||
editor.removeChild(editor.lastChild);
|
|
||||||
--surplusElementCount;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (lines.length) {
|
||||||
|
removeNextSiblings(editor.children[lines.length]);
|
||||||
|
} else {
|
||||||
|
removeChildren(editor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ export function mdSerialize(model) {
|
||||||
return html + "\n";
|
return html + "\n";
|
||||||
case "plain":
|
case "plain":
|
||||||
case "pill-candidate":
|
case "pill-candidate":
|
||||||
|
case "at-room-pill":
|
||||||
return html + part.text;
|
return html + part.text;
|
||||||
case "room-pill":
|
case "room-pill":
|
||||||
case "user-pill":
|
case "user-pill":
|
||||||
|
@ -47,6 +48,7 @@ export function textSerialize(model) {
|
||||||
return text + "\n";
|
return text + "\n";
|
||||||
case "plain":
|
case "plain":
|
||||||
case "pill-candidate":
|
case "pill-candidate":
|
||||||
|
case "at-room-pill":
|
||||||
return text + part.text;
|
return text + part.text;
|
||||||
case "room-pill":
|
case "room-pill":
|
||||||
case "user-pill":
|
case "user-pill":
|
||||||
|
@ -58,13 +60,11 @@ export function textSerialize(model) {
|
||||||
export function requiresHtml(model) {
|
export function requiresHtml(model) {
|
||||||
return model.parts.some(part => {
|
return model.parts.some(part => {
|
||||||
switch (part.type) {
|
switch (part.type) {
|
||||||
case "newline":
|
|
||||||
case "plain":
|
|
||||||
case "pill-candidate":
|
|
||||||
return false;
|
|
||||||
case "room-pill":
|
case "room-pill":
|
||||||
case "user-pill":
|
case "user-pill":
|
||||||
return true;
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue