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/ChangePassword.js
|
||||
src/components/views/settings/DevicesPanel.js
|
||||
src/components/views/settings/IntegrationsManager.js
|
||||
src/components/views/settings/Notifications.js
|
||||
src/GroupAddressPicker.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)
|
||||
===================================================================================================
|
||||
[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",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -75,13 +75,13 @@
|
|||
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279",
|
||||
"gfm.css": "^1.1.1",
|
||||
"glob": "^5.0.14",
|
||||
"highlight.js": "9.14.2",
|
||||
"highlight.js": "^9.15.8",
|
||||
"is-ip": "^2.0.0",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"linkifyjs": "^2.1.6",
|
||||
"lodash": "^4.13.1",
|
||||
"lolex": "2.3.2",
|
||||
"matrix-js-sdk": "2.0.0",
|
||||
"matrix-js-sdk": "2.0.1",
|
||||
"optimist": "^0.6.1",
|
||||
"pako": "^1.0.5",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
|
|
|
@ -50,7 +50,6 @@
|
|||
@import "./views/context_menus/_TopLeftMenu.scss";
|
||||
@import "./views/dialogs/_AddressPickerDialog.scss";
|
||||
@import "./views/dialogs/_Analytics.scss";
|
||||
@import "./views/dialogs/_BugReportDialog.scss";
|
||||
@import "./views/dialogs/_ChangelogDialog.scss";
|
||||
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
|
||||
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
||||
|
@ -62,6 +61,7 @@
|
|||
@import "./views/dialogs/_EncryptedEventDialog.scss";
|
||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||
@import "./views/dialogs/_RestoreKeyBackupDialog.scss";
|
||||
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
||||
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
||||
|
@ -87,6 +87,7 @@
|
|||
@import "./views/elements/_Field.scss";
|
||||
@import "./views/elements/_ImageView.scss";
|
||||
@import "./views/elements/_InlineSpinner.scss";
|
||||
@import "./views/elements/_InteractiveTooltip.scss";
|
||||
@import "./views/elements/_ManageIntegsButton.scss";
|
||||
@import "./views/elements/_MemberEventListSummary.scss";
|
||||
@import "./views/elements/_MessageEditor.scss";
|
||||
|
@ -100,8 +101,8 @@
|
|||
@import "./views/elements/_SyntaxHighlight.scss";
|
||||
@import "./views/elements/_TextWithTooltip.scss";
|
||||
@import "./views/elements/_ToggleSwitch.scss";
|
||||
@import "./views/elements/_ToolTipButton.scss";
|
||||
@import "./views/elements/_Tooltip.scss";
|
||||
@import "./views/elements/_TooltipButton.scss";
|
||||
@import "./views/elements/_Validation.scss";
|
||||
@import "./views/globals/_MatrixToolbar.scss";
|
||||
@import "./views/groups/_GroupPublicityToggle.scss";
|
||||
|
@ -117,7 +118,8 @@
|
|||
@import "./views/messages/_MTextBody.scss";
|
||||
@import "./views/messages/_MessageActionBar.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/_ReactionsRowButton.scss";
|
||||
@import "./views/messages/_ReactionsRowButtonTooltip.scss";
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -58,18 +59,6 @@ limitations under the License.
|
|||
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 {
|
||||
left: 0;
|
||||
}
|
||||
|
@ -89,18 +78,6 @@ limitations under the License.
|
|||
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 {
|
||||
top: 0;
|
||||
}
|
||||
|
@ -120,18 +97,6 @@ limitations under the License.
|
|||
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 {
|
||||
bottom: 0;
|
||||
}
|
||||
|
@ -151,24 +116,6 @@ limitations under the License.
|
|||
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 {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
|
|
|
@ -2,17 +2,15 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mx_GenericErrorPage_box {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
display: inline;
|
||||
width: 500px;
|
||||
height: 125px;
|
||||
min-height: 125px;
|
||||
border: 1px solid #f22;
|
||||
padding: 10px 10px 20px;
|
||||
background-color: #fcc;
|
||||
|
|
|
@ -35,13 +35,6 @@ limitations under the License.
|
|||
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 {
|
||||
background-color: $button-bg-color;
|
||||
border-radius: 4px;
|
||||
|
|
|
@ -63,7 +63,6 @@ limitations under the License.
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ limitations under the License.
|
|||
|
||||
.mx_Login_submit:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mx_AuthBody a.mx_Login_sso_link:link,
|
||||
|
|
|
@ -72,7 +72,6 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_Field input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
@ -110,7 +109,6 @@ limitations under the License.
|
|||
|
||||
.mx_AuthBody_fieldRow > .mx_Field {
|
||||
margin: 0 5px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mx_AuthBody_fieldRow > .mx_Field:first-child {
|
||||
|
|
|
@ -49,10 +49,14 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_InteractiveAuthEntryComponents_termsSubmit:disabled {
|
||||
background-color: $accent-color-50pct;
|
||||
background-color: $accent-color-darker;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mx_InteractiveAuthEntryComponents_termsPolicy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_InteractiveAuthEntryComponents_passwordSection {
|
||||
width: 300px;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_ServerConfig_fields .mx_Field {
|
||||
flex: 1;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,3 +21,7 @@ limitations under the License.
|
|||
.mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.mx_DeactivateAccountDialog .mx_DeactivateAccountDialog_input_section .mx_Field {
|
||||
width: 300px;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,11 @@ limitations under the License.
|
|||
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;
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -75,11 +79,6 @@ limitations under the License.
|
|||
max-width: 684px;
|
||||
min-height: 250px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_DevTools_content .mx_Field_input {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.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;
|
||||
background-color: $primary-bg-color;
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
|
|
@ -42,12 +42,6 @@ limitations under the License.
|
|||
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 {
|
||||
margin-bottom: 5px;
|
||||
}
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
|||
|
||||
.mx_Field {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
margin: 1em 0;
|
||||
border-radius: 4px;
|
||||
|
@ -42,6 +44,7 @@ limitations under the License.
|
|||
padding: 8px 9px;
|
||||
color: $primary-fg-color;
|
||||
background-color: $primary-bg-color;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow-x: auto;
|
||||
|
||||
&:focus {
|
||||
border-color: $accent-color-50pct;
|
||||
}
|
||||
|
||||
span.mx_UserPill, span.mx_RoomPill {
|
||||
padding-left: 21px;
|
||||
position: relative;
|
||||
|
|
|
@ -20,6 +20,5 @@ limitations under the License.
|
|||
|
||||
.mx_PowerSelector .mx_Field select,
|
||||
.mx_PowerSelector .mx_Field input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
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.
|
||||
|
@ -14,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_ToolTipButton {
|
||||
.mx_TooltipButton {
|
||||
display: inline-block;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
|
@ -33,17 +34,17 @@ limitations under the License.
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_ToolTipButton:hover {
|
||||
.mx_TooltipButton:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.mx_ToolTipButton_container {
|
||||
.mx_TooltipButton_container {
|
||||
position: relative;
|
||||
top: -18px;
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.mx_ToolTipButton_helpText {
|
||||
.mx_TooltipButton_helpText {
|
||||
width: 400px;
|
||||
text-align: start;
|
||||
line-height: 17px !important;
|
|
@ -67,6 +67,10 @@ limitations under the License.
|
|||
background-color: $message-action-bar-fg-color;
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_reactButton::after {
|
||||
mask-image: url('$(res)/img/react.svg');
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_replyButton::after {
|
||||
mask-image: url('$(res)/img/reply.svg');
|
||||
}
|
||||
|
|
|
@ -15,4 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.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");
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_BugReportDialog .mx_Field {
|
||||
flex: 1;
|
||||
.mx_ReactionsQuickTooltip_buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, auto);
|
||||
}
|
||||
|
||||
.mx_BugReportDialog_field_input {
|
||||
// TODO: We should really apply this to all .mx_Field inputs.
|
||||
// See https://github.com/vector-im/riot-web/issues/9344.
|
||||
flex: 1;
|
||||
.mx_ReactionsQuickTooltip_label {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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");
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_ReactionDimension {
|
||||
width: 42px;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
.mx_ReactionTooltipButton {
|
||||
font-size: 16px;
|
||||
padding: 6px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.25s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ReactionDimension_disabled {
|
||||
.mx_ReactionTooltipButton_selected {
|
||||
opacity: 0.4;
|
||||
}
|
|
@ -18,3 +18,17 @@ limitations under the License.
|
|||
margin: 6px 0;
|
||||
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;
|
||||
visibility: hidden;
|
||||
white-space: nowrap;
|
||||
color: $event-timestamp-color;
|
||||
font-size: 10px;
|
||||
left: 0px;
|
||||
width: 46px; /* 8 + 30 (avatar) + 8 */
|
||||
text-align: center;
|
||||
|
@ -169,6 +167,9 @@ limitations under the License.
|
|||
.mx_EventTile_sending .mx_RoomPill {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.mx_EventTile_sending.mx_EventTile_redacted .mx_UnknownBody {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.mx_EventTile_notSent {
|
||||
color: $event-notsent-color;
|
||||
|
@ -400,6 +401,7 @@ limitations under the License.
|
|||
color: $roomtopic-color;
|
||||
display: inline-block;
|
||||
margin-left: 9px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Various markdown overrides */
|
||||
|
|
|
@ -43,6 +43,8 @@ limitations under the License.
|
|||
|
||||
.mx_MemberInfo_name h2 {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.mx_MemberInfo h2 {
|
||||
|
|
|
@ -39,6 +39,16 @@ limitations under the License.
|
|||
margin: 10px 10px 10px 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_footer {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
|
||||
.mx_Spinner {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_dark {
|
||||
|
|
|
@ -15,17 +15,22 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.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;
|
||||
height: 235px;
|
||||
background-color: $event-selected-color;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
background-color: $preview-bar-bg-color;
|
||||
-webkit-align-items: center;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.mx_RoomUpgradeWarningBar_header {
|
||||
|
|
|
@ -35,9 +35,3 @@ limitations under the License.
|
|||
.mx_ExistingEmailAddress_confirmBtn {
|
||||
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%;
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -22,11 +22,6 @@ limitations under the License.
|
|||
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 {
|
||||
height: 4em;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,3 @@ limitations under the License.
|
|||
.mx_GeneralRoomSettingsTab_profileSection {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mx_GeneralRoomSettingsTab .mx_AliasSettings .mx_Field select {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -14,31 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_GeneralUserSettingsTab_changePassword,
|
||||
.mx_GeneralUserSettingsTab_themeSection {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_GeneralUserSettingsTab_changePassword .mx_Field,
|
||||
.mx_GeneralUserSettingsTab_themeSection .mx_Field {
|
||||
display: block;
|
||||
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 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mx_GeneralUserSettingsTab_themeSection .mx_Field select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses,
|
||||
.mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers,
|
||||
.mx_GeneralUserSettingsTab_languageInput {
|
||||
|
|
|
@ -17,11 +17,3 @@ limitations under the License.
|
|||
.mx_PreferencesUserSettingsTab .mx_Field {
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_VoiceUserSettingsTab .mx_Field select {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mx_VoiceUserSettingsTab .mx_Field {
|
||||
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-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: $base-color;
|
||||
$interactive-tooltip-fg-color: #ffffff;
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
|
|
@ -28,7 +28,8 @@ $focus-bg-color: #dddddd;
|
|||
|
||||
// button UI (white-on-green in light skin)
|
||||
$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;
|
||||
|
||||
$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-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: #27303a;
|
||||
$interactive-tooltip-fg-color: #ffffff;
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
|
|
@ -344,7 +344,7 @@ function _onAction(payload) {
|
|||
}
|
||||
|
||||
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
|
||||
// work for us. Better that the user knows before everyone else in the
|
||||
// room sees it.
|
||||
|
|
|
@ -18,6 +18,11 @@ import * as Matrix from 'matrix-js-sdk';
|
|||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
|
||||
export default {
|
||||
hasAnyLabeledDevices: async function() {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.some(d => !!d.label);
|
||||
},
|
||||
|
||||
getDevices: function() {
|
||||
// Only needed for Electron atm, though should work in modern browsers
|
||||
// once permission has been granted to the webapp
|
||||
|
@ -26,8 +31,6 @@ export default {
|
|||
const audioinput = [];
|
||||
const videoinput = [];
|
||||
|
||||
if (devices.some((device) => !device.label)) return false;
|
||||
|
||||
devices.forEach((device) => {
|
||||
switch (device.kind) {
|
||||
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");
|
||||
let uploadAll = false;
|
||||
for (let i = 0; i < okFiles.length; ++i) {
|
||||
const file = okFiles[i];
|
||||
if (!uploadAll) {
|
||||
const shouldContinue = await new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
|
||||
file,
|
||||
currentIndex: i,
|
||||
totalFiles: okFiles.length,
|
||||
onFinished: (shouldContinue) => {
|
||||
onFinished: (shouldContinue, shouldUploadAll) => {
|
||||
if (shouldUploadAll) {
|
||||
uploadAll = true;
|
||||
}
|
||||
resolve(shouldContinue);
|
||||
},
|
||||
});
|
||||
});
|
||||
if (!shouldContinue) break;
|
||||
}
|
||||
this._sendContentToRoom(file, roomId, matrixClient);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,12 @@ limitations under the License.
|
|||
|
||||
import URL from 'url';
|
||||
import dis from './dispatcher';
|
||||
import IntegrationManager from './IntegrationManager';
|
||||
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
||||
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 SUPPORTED_WIDGET_API_VERSIONS = [
|
||||
|
@ -189,7 +192,14 @@ export default class FromWidgetPostMessageApi {
|
|||
const data = event.data.data || event.data.widgetData;
|
||||
const integType = (data && data.integType) ? data.integType : 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') {
|
||||
// This is a new message: there is no reason to support the deprecated widgetData here
|
||||
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 {
|
||||
constructor() {
|
||||
this.matrixClient = null;
|
||||
this._justRegisteredUserId = null;
|
||||
|
||||
// These are the default options used when when the
|
||||
// client is started in 'start'. These can be altered
|
||||
|
@ -85,6 +86,31 @@ class MatrixClientPeg {
|
|||
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
|
||||
* homeserver / identity server URLs and active credentials
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -26,13 +27,33 @@ export const MUTE = 'mute';
|
|||
export const BADGE_STATES = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
||||
export const MENTION_BADGE_STATES = [...BADGE_STATES, MENTIONS_ONLY];
|
||||
|
||||
function _shouldShowNotifBadge(roomNotifState) {
|
||||
const showBadgeInStates = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
||||
return showBadgeInStates.indexOf(roomNotifState) > -1;
|
||||
export function shouldShowNotifBadge(roomNotifState) {
|
||||
return BADGE_STATES.includes(roomNotifState);
|
||||
}
|
||||
|
||||
function _shouldShowMentionBadge(roomNotifState) {
|
||||
return roomNotifState !== MUTE;
|
||||
export function shouldShowMentionBadge(roomNotifState) {
|
||||
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) {
|
||||
|
@ -41,8 +62,8 @@ export function aggregateNotificationCount(rooms) {
|
|||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && _shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && _shouldShowMentionBadge(roomNotifState);
|
||||
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState);
|
||||
const badges = notifBadges || mentionBadges;
|
||||
|
||||
if (badges) {
|
||||
|
@ -60,8 +81,8 @@ export function getRoomHasBadge(room) {
|
|||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && _shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && _shouldShowMentionBadge(roomNotifState);
|
||||
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState);
|
||||
|
||||
return notifBadges || mentionBadges;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,14 @@ class ScalarAuthClient {
|
|||
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() {
|
||||
return this.getScalarToken().then((tok) => {
|
||||
this.scalarToken = tok;
|
||||
|
@ -41,7 +49,8 @@ class ScalarAuthClient {
|
|||
|
||||
// Returns a scalar_token string
|
||||
getScalarToken() {
|
||||
const token = window.localStorage.getItem("mx_scalar_token");
|
||||
let token = this.scalarToken;
|
||||
if (!token) token = window.localStorage.getItem("mx_scalar_token");
|
||||
|
||||
if (!token) {
|
||||
return this.registerForToken();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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");
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -48,7 +48,6 @@ export default class ContextualMenu extends React.Component {
|
|||
menuWidth: PropTypes.number,
|
||||
menuHeight: PropTypes.number,
|
||||
chevronOffset: PropTypes.number,
|
||||
menuColour: PropTypes.string,
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
||||
// Function to be called on menu close
|
||||
onFinished: PropTypes.func,
|
||||
|
@ -157,25 +156,6 @@ export default class ContextualMenu extends React.Component {
|
|||
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 ?
|
||||
<div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} /> :
|
||||
undefined;
|
||||
|
@ -202,10 +182,6 @@ export default class ContextualMenu extends React.Component {
|
|||
menuStyle.height = props.menuHeight;
|
||||
}
|
||||
|
||||
if (props.menuColour) {
|
||||
menuStyle["backgroundColor"] = props.menuColour;
|
||||
}
|
||||
|
||||
if (!isNaN(Number(props.menuPaddingTop))) {
|
||||
menuStyle["paddingTop"] = props.menuPaddingTop;
|
||||
}
|
||||
|
@ -236,7 +212,6 @@ export default class ContextualMenu extends React.Component {
|
|||
</div>
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
|
||||
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||
<style>{ chevronCSS }</style>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations 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");
|
||||
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 TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
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(
|
||||
'These rooms are displayed to community members on the community page. '+
|
||||
'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._scrollElement = null;
|
||||
this._autoHideScrollbar = null;
|
||||
this._likelyTrackpadUser = null;
|
||||
this._checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
||||
|
||||
this.state = {
|
||||
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.
|
||||
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.
|
||||
// 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
|
||||
|
|
|
@ -292,16 +292,6 @@ const LoggedInView = React.createClass({
|
|||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
|
||||
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_DOWN:
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
|
|
|
@ -53,6 +53,8 @@ import { messageForSyncError } from '../../utils/ErrorUtils';
|
|||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import { ValidatedServerConfig } 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
|
||||
// and need to migrate, but they spam the console with warnings.
|
||||
|
@ -887,6 +889,7 @@ export default React.createClass({
|
|||
}
|
||||
return;
|
||||
}
|
||||
MatrixClientPeg.setJustRegisteredUserId(credentials.user_id);
|
||||
this.onRegistered(credentials);
|
||||
},
|
||||
onDifferentServerClicked: (ev) => {
|
||||
|
@ -1132,28 +1135,80 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
/**
|
||||
* Called when a new logged in session has started
|
||||
* 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
|
||||
*/
|
||||
_onLoggedIn: async function() {
|
||||
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
|
||||
if (this._is_registered) {
|
||||
this._is_registered = false;
|
||||
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;
|
||||
|
||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||
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
|
||||
});
|
||||
// if successful, return because we're already
|
||||
// viewing the welcomeUserId room
|
||||
// else, if failed, fall through to view_home_page
|
||||
if (roomId) {
|
||||
return;
|
||||
// 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;
|
||||
}
|
||||
// The user has just logged in after registering
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a new logged in session has started
|
||||
*/
|
||||
_onLoggedIn: async function() {
|
||||
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
|
||||
if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
||||
MatrixClientPeg.setJustRegisteredUserId(null);
|
||||
|
||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||
const welcomeUserRoom = await this._startWelcomeUserChat();
|
||||
if (welcomeUserRoom === null) {
|
||||
// We didn't rediret to the welcome user room, so show
|
||||
// the homepage.
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
}
|
||||
} else {
|
||||
// The user has just logged in after registering,
|
||||
// so show the homepage.
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
}
|
||||
} else {
|
||||
this._showScreenAfterLogin();
|
||||
}
|
||||
|
@ -1655,48 +1710,6 @@ export default React.createClass({
|
|||
|
||||
// returns a promise which resolves to the new MatrixClient
|
||||
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);
|
||||
},
|
||||
|
||||
|
@ -1737,19 +1750,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
updateStatusIndicator: function(state, prevState) {
|
||||
let notifCount = 0;
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getRooms()).count;
|
||||
|
||||
if (PlatformPeg.get()) {
|
||||
PlatformPeg.get().setErrorStatus(state === 'ERROR');
|
||||
|
|
|
@ -108,6 +108,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing");
|
||||
// the event after which we put a visible unread marker on the last
|
||||
// render cycle; null if readMarkerVisible was false or the RM was
|
||||
// suppressed (eg because it was at the end of the timeline)
|
||||
|
@ -585,14 +586,14 @@ module.exports = React.createClass({
|
|||
<EventTile mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
replacingEventId={this._editingEnabled && mxEv.replacingEventId()}
|
||||
editState={isEditing && this.props.editState}
|
||||
onHeightChanged={this._onHeightChanged}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting}
|
||||
eventSendStatus={mxEv.replacementOrOwnStatus()}
|
||||
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
|
|
|
@ -333,7 +333,7 @@ module.exports = React.createClass({
|
|||
if (alias.indexOf(':') == -1) {
|
||||
alias = alias + ':' + this.state.roomServer;
|
||||
}
|
||||
this.showRoomAlias(alias);
|
||||
this.showRoomAlias(alias, true);
|
||||
} else {
|
||||
// This is a 3rd party protocol. Let's see if we can join it
|
||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||
|
@ -349,7 +349,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => {
|
||||
if (resp.length > 0 && resp[0].alias) {
|
||||
this.showRoomAlias(resp[0].alias);
|
||||
this.showRoomAlias(resp[0].alias, true);
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
||||
|
@ -367,13 +367,16 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
showRoomAlias: function(alias) {
|
||||
this.showRoom(null, alias);
|
||||
showRoomAlias: function(alias, autoJoin=false) {
|
||||
this.showRoom(null, alias, autoJoin);
|
||||
},
|
||||
|
||||
showRoom: function(room, room_alias) {
|
||||
showRoom: function(room, room_alias, autoJoin=false) {
|
||||
this.props.onFinished();
|
||||
const payload = {action: 'view_room'};
|
||||
const payload = {
|
||||
action: 'view_room',
|
||||
auto_join: autoJoin,
|
||||
};
|
||||
if (room) {
|
||||
// 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
|
||||
|
|
|
@ -1523,9 +1523,11 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomView">
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
previewLoading={this.state.peekLoading}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
oobData={this.props.oobData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -29,6 +29,7 @@ import * as ServerType from '../../views/auth/ServerTypeSelector';
|
|||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from "classnames";
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
// Phases
|
||||
// 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
|
||||
// component without it.
|
||||
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) {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
serverDeadError: null,
|
||||
serverErrorIsFatal: false,
|
||||
// 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)
|
||||
busy: true,
|
||||
|
@ -179,7 +189,10 @@ module.exports = React.createClass({
|
|||
serverConfig.hsUrl,
|
||||
serverConfig.isUrl,
|
||||
);
|
||||
this.setState({serverIsAlive: true});
|
||||
this.setState({
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
|
@ -287,9 +300,29 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
MatrixClientPeg.setJustRegisteredUserId(response.user_id);
|
||||
|
||||
const newState = {
|
||||
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) {
|
||||
const cli = await this.props.onLoggedIn({
|
||||
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,
|
||||
// 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>
|
||||
<ServerTypeSelector
|
||||
selected={this.state.serverType}
|
||||
|
@ -528,6 +563,7 @@ module.exports = React.createClass({
|
|||
const AuthHeader = sdk.getComponent('auth.AuthHeader');
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const AuthPage = sdk.getComponent('auth.AuthPage');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let errorText;
|
||||
const err = this.state.errorText;
|
||||
|
@ -564,28 +600,41 @@ module.exports = React.createClass({
|
|||
let body;
|
||||
if (this.state.completedNoSignin) {
|
||||
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
|
||||
regDoneText = _t(
|
||||
regDoneText = <h3>{_t(
|
||||
"<a>Log in</a> to your new account.", {},
|
||||
{
|
||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
||||
},
|
||||
);
|
||||
)}</h3>;
|
||||
} else {
|
||||
// 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
|
||||
// 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.", {},
|
||||
{
|
||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
||||
},
|
||||
);
|
||||
)}</h3>;
|
||||
}
|
||||
body = <div>
|
||||
<h2>{_t("Registration Successful")}</h2>
|
||||
<h3>{ regDoneText }</h3>
|
||||
{ regDoneText }
|
||||
</div>;
|
||||
} else {
|
||||
body = <div>
|
||||
|
|
|
@ -81,16 +81,10 @@ export const PasswordAuthEntry = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
passwordValid: false,
|
||||
password: "",
|
||||
};
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.refs.passwordField) {
|
||||
this.refs.passwordField.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_onSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.busy) return;
|
||||
|
@ -98,23 +92,21 @@ export const PasswordAuthEntry = React.createClass({
|
|||
this.props.submitAuthDict({
|
||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
||||
user: this.props.matrixClient.credentials.userId,
|
||||
password: this.refs.passwordField.value,
|
||||
password: this.state.password,
|
||||
});
|
||||
},
|
||||
|
||||
_onPasswordFieldChange: function(ev) {
|
||||
// enable the submit button iff the password is non-empty
|
||||
this.setState({
|
||||
passwordValid: Boolean(this.refs.passwordField.value),
|
||||
password: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let passwordBoxClass = null;
|
||||
|
||||
if (this.props.errorText) {
|
||||
passwordBoxClass = 'error';
|
||||
}
|
||||
const passwordBoxClass = classnames({
|
||||
"error": this.props.errorText,
|
||||
});
|
||||
|
||||
let submitButtonOrSpinner;
|
||||
if (this.props.busy) {
|
||||
|
@ -124,7 +116,7 @@ export const PasswordAuthEntry = React.createClass({
|
|||
submitButtonOrSpinner = (
|
||||
<input type="submit"
|
||||
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 (
|
||||
<div>
|
||||
<p>{ _t("To continue, please enter your password.") }</p>
|
||||
<form onSubmit={this._onSubmit}>
|
||||
<label htmlFor="passwordField">{ _t("Password:") }</label>
|
||||
<input
|
||||
name="passwordField"
|
||||
ref="passwordField"
|
||||
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||
<Field
|
||||
id="mx_InteractiveAuthEntryComponents_password"
|
||||
className={passwordBoxClass}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
type="password"
|
||||
name="passwordField"
|
||||
label={_t('Password')}
|
||||
autoFocus={true}
|
||||
value={this.state.password}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
/>
|
||||
<div className="mx_button_row">
|
||||
{ submitButtonOrSpinner }
|
||||
|
|
|
@ -104,6 +104,9 @@ export default class ServerConfig extends React.PureComponent {
|
|||
|
||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
if (!stateForError.isFatalError) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
// carry on anyway
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
|
||||
this.props.onServerConfigChange(result);
|
||||
|
|
|
@ -36,7 +36,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
confirmButtonEnabled: false,
|
||||
password: "",
|
||||
busy: false,
|
||||
shouldErase: false,
|
||||
errStr: null,
|
||||
|
@ -45,7 +45,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
|
||||
_onPasswordFieldChange(ev) {
|
||||
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 okEnabled = this.state.confirmButtonEnabled && !this.state.busy;
|
||||
const okEnabled = this.state.password && !this.state.busy;
|
||||
|
||||
let cancelButton = null;
|
||||
if (!this.state.busy) {
|
||||
|
@ -113,6 +113,8 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
</button>;
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
|
@ -167,10 +169,12 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
</p>
|
||||
|
||||
<p>{ _t("To continue, please enter your password:") }</p>
|
||||
<input
|
||||
<Field
|
||||
id="mx_DeactivateAccountDialog_password"
|
||||
type="password"
|
||||
placeholder={_t("password")}
|
||||
label={_t('Password')}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
value={this.state.password}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
|
|
|
@ -34,9 +34,15 @@ export default class IncomingSasDialog extends React.Component {
|
|||
constructor(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.state = {
|
||||
phase: PHASE_START,
|
||||
phase: phase,
|
||||
sasVerified: false,
|
||||
opponentProfile: 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>
|
||||
{_t(
|
||||
"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:",
|
||||
)}
|
||||
</p>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,6 +19,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import filesize from "filesize";
|
||||
|
||||
export default class UploadConfirmDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -49,6 +51,10 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_onUploadAllClick = () => {
|
||||
this.props.onFinished(true, true);
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
@ -71,7 +77,7 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
preview = <div className="mx_UploadConfirmDialog_previewOuter">
|
||||
<div className="mx_UploadConfirmDialog_previewInner">
|
||||
<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>;
|
||||
} else {
|
||||
|
@ -80,11 +86,18 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
<img className="mx_UploadConfirmDialog_fileIcon"
|
||||
src={require("../../../../res/img/files.png")}
|
||||
/>
|
||||
{this.props.file.name}
|
||||
{this.props.file.name} ({filesize(this.props.file.size)})
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let uploadAllButton;
|
||||
if (this.props.currentIndex + 1 < this.props.totalFiles) {
|
||||
uploadAllButton = <button onClick={this._onUploadAllClick}>
|
||||
{_t("Upload all")}
|
||||
</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_UploadConfirmDialog'
|
||||
fixedWidth={false}
|
||||
|
@ -100,7 +113,9 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
hasCancel={false}
|
||||
onPrimaryButtonClick={this._onUploadClick}
|
||||
focus={true}
|
||||
/>
|
||||
>
|
||||
{uploadAllButton}
|
||||
</DialogButtons>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -240,19 +240,13 @@ export default class AppTile extends React.Component {
|
|||
if (this.props.onEditClick) {
|
||||
this.props.onEditClick();
|
||||
} else {
|
||||
// The dialog handles scalar auth for us
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
this._scalarClient.connect().done(() => {
|
||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room, 'type_' + this.props.type, this.props.id);
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
room: this.props.room,
|
||||
screen: 'type_' + this.props.type,
|
||||
integrationId: this.props.id,
|
||||
}, "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 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.
|
||||
|
@ -17,95 +18,34 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import classNames from 'classnames';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import Modal from "../../../Modal";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
|
||||
export default class ManageIntegsButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
scalarError: null,
|
||||
};
|
||||
|
||||
this.onManageIntegrations = this.onManageIntegrations.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
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) {
|
||||
onManageIntegrations = (ev) => {
|
||||
ev.preventDefault();
|
||||
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
this.scalarClient.connect().done(() => {
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
|
||||
null,
|
||||
room: this.props.room,
|
||||
}, "mx_IntegrationsManager");
|
||||
}, (err) => {
|
||||
this.setState({scalarError: err});
|
||||
console.error('Error ensuring a valid scalar_token exists', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let integrationsButton = <div />;
|
||||
let integrationsWarningTriangle = <div />;
|
||||
let integrationsErrorPopup = <div />;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
if (ScalarAuthClient.isPossible()) {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
integrationsButton = (
|
||||
<AccessibleButton className={integrationsButtonClasses}
|
||||
<AccessibleButton
|
||||
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
|
||||
title={_t("Manage Integrations")}
|
||||
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);
|
||||
}
|
||||
|
||||
_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() {
|
||||
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
|
||||
return caret.offset === 0;
|
||||
|
@ -92,7 +100,7 @@ export default class MessageEditor extends React.Component {
|
|||
// insert newline on Shift+Enter
|
||||
if (event.shiftKey && event.key === "Enter") {
|
||||
event.preventDefault(); // just in case the browser does support this
|
||||
document.execCommand("insertHTML", undefined, "\n");
|
||||
this._insertText("\n");
|
||||
return;
|
||||
}
|
||||
// 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'});
|
||||
}
|
||||
|
||||
_isEmote() {
|
||||
const firstPart = this.model.parts[0];
|
||||
return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me ");
|
||||
}
|
||||
|
||||
_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 = {
|
||||
"msgtype": "m.text",
|
||||
"body": textSerialize(this.model),
|
||||
"msgtype": isEmote ? "m.emote" : "m.text",
|
||||
"body": textSerialize(model),
|
||||
};
|
||||
const contentBody = {
|
||||
msgtype: newContent.msgtype,
|
||||
body: ` * ${newContent.body}`,
|
||||
};
|
||||
const formattedBody = htmlSerializeIfNeeded(this.model);
|
||||
const formattedBody = htmlSerializeIfNeeded(model);
|
||||
if (formattedBody) {
|
||||
newContent.format = "org.matrix.custom.html";
|
||||
newContent.formatted_body = formattedBody;
|
||||
|
@ -232,7 +252,7 @@ export default class MessageEditor extends React.Component {
|
|||
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||
} else {
|
||||
// otherwise, parse the body of the event
|
||||
parts = parseEvent(editState.getEvent(), room, this.context.matrixClient);
|
||||
parts = parseEvent(editState.getEvent(), partCreator);
|
||||
}
|
||||
|
||||
return new EditorModel(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
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.
|
||||
|
@ -18,7 +19,7 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ToolTipButton',
|
||||
displayName: 'TooltipButton',
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
|
@ -41,12 +42,12 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_ToolTipButton_container"
|
||||
tooltipClassName="mx_ToolTipButton_helpText"
|
||||
className="mx_TooltipButton_container"
|
||||
tooltipClassName="mx_TooltipButton_helpText"
|
||||
label={this.props.helpText}
|
||||
/> : <div />;
|
||||
return (
|
||||
<div className="mx_ToolTipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
|
||||
<div className="mx_TooltipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
|
||||
?
|
||||
{ tip }
|
||||
</div>
|
|
@ -224,7 +224,7 @@ module.exports = React.createClass({
|
|||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ this.state.groupRoom.canonical_alias }
|
||||
{ this.state.groupRoom.canonicalAlias }
|
||||
</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);
|
||||
}
|
||||
|
||||
onCryptoClicked = () => {
|
||||
onCryptoClick = () => {
|
||||
const event = this.props.mxEvent;
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||
|
@ -89,7 +89,7 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
|
||||
let e2eInfoCallback = null;
|
||||
if (this.props.mxEvent.isEncrypted()) {
|
||||
e2eInfoCallback = () => this.onCryptoClicked();
|
||||
e2eInfoCallback = () => this.onCryptoClick();
|
||||
}
|
||||
|
||||
const menuOptions = {
|
||||
|
@ -131,43 +131,28 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
return SettingsStore.isFeatureEnabled("feature_message_editing");
|
||||
}
|
||||
|
||||
renderAgreeDimension() {
|
||||
renderReactButton() {
|
||||
if (!this.isReactionsEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
|
||||
return <ReactionDimension
|
||||
title={_t("Agree or Disagree")}
|
||||
options={["👍", "👎"]}
|
||||
reactions={this.props.reactions}
|
||||
mxEvent={this.props.mxEvent}
|
||||
/>;
|
||||
}
|
||||
const ReactMessageAction = sdk.getComponent('messages.ReactMessageAction');
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
renderLikeDimension() {
|
||||
if (!this.isReactionsEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
|
||||
return <ReactionDimension
|
||||
title={_t("Like or Dislike")}
|
||||
options={["🙂", "😔"]}
|
||||
reactions={this.props.reactions}
|
||||
mxEvent={this.props.mxEvent}
|
||||
return <ReactMessageAction
|
||||
mxEvent={mxEvent}
|
||||
reactions={reactions}
|
||||
onFocusChange={this.onFocusChange}
|
||||
/>;
|
||||
}
|
||||
|
||||
render() {
|
||||
let agreeDimensionReactionButtons;
|
||||
let likeDimensionReactionButtons;
|
||||
let reactButton;
|
||||
let replyButton;
|
||||
let editButton;
|
||||
|
||||
if (isContentActionable(this.props.mxEvent)) {
|
||||
agreeDimensionReactionButtons = this.renderAgreeDimension();
|
||||
likeDimensionReactionButtons = this.renderLikeDimension();
|
||||
reactButton = this.renderReactButton();
|
||||
replyButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
title={_t("Reply")}
|
||||
onClick={this.onReplyClick}
|
||||
|
@ -181,8 +166,7 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
return <div className="mx_MessageActionBar">
|
||||
{agreeDimensionReactionButtons}
|
||||
{likeDimensionReactionButtons}
|
||||
{reactButton}
|
||||
{replyButton}
|
||||
{editButton}
|
||||
<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 sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import { isSingleEmoji } from '../../../HtmlUtils';
|
||||
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 {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
|
@ -41,6 +45,7 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
|
||||
this.state = {
|
||||
myReactions: this.getMyReactions(),
|
||||
showAll: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -94,16 +99,22 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onShowAllClick = () => {
|
||||
this.setState({
|
||||
showAll: true,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mxEvent, reactions } = this.props;
|
||||
const { myReactions } = this.state;
|
||||
const { myReactions, showAll } = this.state;
|
||||
|
||||
if (!reactions || !isContentActionable(mxEvent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
|
||||
const items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||
if (!isSingleEmoji(content)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -125,10 +136,26 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
reactionEvents={events}
|
||||
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">
|
||||
{items}
|
||||
{showAllButton}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,12 +30,11 @@ import Modal from '../../../Modal';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import * as ContextualMenu from '../../structures/ContextualMenu';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {host as matrixtoHost} from '../../../matrix-to';
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TextualBody',
|
||||
|
@ -99,7 +98,7 @@ module.exports = React.createClass({
|
|||
// 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,
|
||||
// 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);
|
||||
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) {
|
||||
let links = [];
|
||||
|
||||
|
@ -448,6 +355,11 @@ module.exports = React.createClass({
|
|||
this.setState({editedMarkerHovered: false});
|
||||
},
|
||||
|
||||
_openHistoryDialog: async function() {
|
||||
const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
|
||||
Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent});
|
||||
},
|
||||
|
||||
_renderEditedMarker: function() {
|
||||
let editedTooltip;
|
||||
if (this.state.editedMarkerHovered) {
|
||||
|
@ -456,12 +368,13 @@ module.exports = React.createClass({
|
|||
const date = editEvent && formatDate(editEvent.getDate());
|
||||
editedTooltip = <Tooltip
|
||||
tooltipClassName="mx_Tooltip_timeline"
|
||||
label={_t("Edited at %(date)s", {date})}
|
||||
label={_t("Edited at %(date)s. Click to view edits.", {date})}
|
||||
/>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key="editedMarker" className="mx_EventTile_edited"
|
||||
onClick={this._openHistoryDialog}
|
||||
onMouseEnter={this._onMouseEnterEditedMarker}
|
||||
onMouseLeave={this._onMouseLeaveEditedMarker}
|
||||
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
|
||||
|
|
|
@ -24,8 +24,6 @@ import AppTile from '../elements/AppTile';
|
|||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
|
@ -63,20 +61,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
|
@ -144,16 +128,10 @@ module.exports = React.createClass({
|
|||
|
||||
_launchManageIntegrations: function() {
|
||||
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
|
||||
this.scalarClient.connect().done(() => {
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') :
|
||||
null;
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
room: this.props.room,
|
||||
screen: 'add_integ',
|
||||
}, 'mx_IntegrationsManager');
|
||||
}, (err) => {
|
||||
console.error('Error ensuring a valid scalar_token exists', err);
|
||||
});
|
||||
},
|
||||
|
||||
onClickAddWidget: function(e) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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");
|
||||
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;
|
||||
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
|
@ -439,7 +440,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
_renderE2EPadlock: function() {
|
||||
const ev = this.props.mxEvent;
|
||||
const props = {onClick: this.onCryptoClicked};
|
||||
const props = {onClick: this.onCryptoClick};
|
||||
|
||||
// event could not be decrypted
|
||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||
|
@ -670,13 +671,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a>},
|
||||
);
|
||||
|
||||
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
|
||||
const TooltipButton = sdk.getComponent('elements.TooltipButton');
|
||||
const keyRequestInfo = isEncryptionFailure ?
|
||||
<div className="mx_EventTile_keyRequestInfo">
|
||||
<span className="mx_EventTile_keyRequestInfo_text">
|
||||
{ keyRequestInfoContent }
|
||||
</span>
|
||||
<ToolTipButton helpText={keyRequestHelpText} />
|
||||
<TooltipButton helpText={keyRequestHelpText} />
|
||||
</div> : null;
|
||||
|
||||
let reactionsRow;
|
||||
|
@ -828,7 +829,7 @@ module.exports.haveTileForEvent = function(e) {
|
|||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||
|
||||
// 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);
|
||||
if (handler === undefined) return false;
|
||||
|
|
|
@ -60,6 +60,7 @@ import ReplyThread from "../elements/ReplyThread";
|
|||
import {ContentHelpers} from 'matrix-js-sdk';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
import ComposerHistoryManager from "../../../ComposerHistoryManager";
|
||||
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
|
||||
|
@ -140,6 +141,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
client: MatrixClient;
|
||||
autocomplete: Autocomplete;
|
||||
historyManager: ComposerHistoryManager;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -329,6 +331,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
componentWillMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -679,14 +682,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
if (this.autocomplete.countCompletions() > 0) {
|
||||
if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) {
|
||||
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:
|
||||
this.autocomplete.moveSelection(-1);
|
||||
ev.preventDefault();
|
||||
|
@ -1062,6 +1057,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
if (cmd) {
|
||||
if (!cmd.error) {
|
||||
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||
this.setState({
|
||||
editorState: this.createEditorState(),
|
||||
}, ()=>{
|
||||
|
@ -1139,6 +1135,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||
|
||||
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||
|
||||
if (commandText && commandText.startsWith('/me')) {
|
||||
if (replyingToEv) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -1198,19 +1196,31 @@ export default class MessageComposerInput extends React.Component {
|
|||
};
|
||||
|
||||
onVerticalArrow = (e, up) => {
|
||||
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
|
||||
|
||||
// Select history
|
||||
const selection = this.state.editorState.selection;
|
||||
if (e.ctrlKey || e.shiftKey || e.metaKey) return;
|
||||
|
||||
// selection must be collapsed
|
||||
const selection = this.state.editorState.selection;
|
||||
if (!selection.isCollapsed) return;
|
||||
const document = this.state.editorState.document;
|
||||
|
||||
// and we must be at the edge of the document (up=start, down=end)
|
||||
const document = this.state.editorState.document;
|
||||
if (up) {
|
||||
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);
|
||||
if (editEvent) {
|
||||
// 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) => {
|
||||
this.setState({
|
||||
someCompletions: null,
|
||||
|
|
|
@ -66,6 +66,7 @@ module.exports = React.createClass({
|
|||
error: PropTypes.object,
|
||||
|
||||
canPreview: PropTypes.bool,
|
||||
previewLoading: PropTypes.bool,
|
||||
room: PropTypes.object,
|
||||
|
||||
// When a spinner is present, a spinnerState can be specified to indicate the
|
||||
|
@ -254,6 +255,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
||||
let showSpinner = false;
|
||||
let darkStyle = false;
|
||||
let title;
|
||||
|
@ -262,6 +265,7 @@ module.exports = React.createClass({
|
|||
let primaryActionLabel;
|
||||
let secondaryActionHandler;
|
||||
let secondaryActionLabel;
|
||||
let footer;
|
||||
|
||||
const messageCase = this._getMessageCase();
|
||||
switch (messageCase) {
|
||||
|
@ -287,6 +291,14 @@ module.exports = React.createClass({
|
|||
primaryActionHandler = this.onRegisterClick;
|
||||
secondaryActionLabel = _t("Sign In");
|
||||
secondaryActionHandler = this.onLoginClick;
|
||||
if (this.props.previewLoading) {
|
||||
footer = (
|
||||
<div>
|
||||
<Spinner w={20} h={20}/>
|
||||
{_t("Loading room preview")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.Kicked: {
|
||||
|
@ -433,7 +445,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
||||
let subTitleElements;
|
||||
if (subTitle) {
|
||||
|
@ -484,6 +495,9 @@ module.exports = React.createClass({
|
|||
{ secondaryButton }
|
||||
{ primaryButton }
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_footer">
|
||||
{ footer }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
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");
|
||||
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) {
|
||||
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
return Boolean(dmRooms);
|
||||
|
@ -301,8 +294,8 @@ module.exports = React.createClass({
|
|||
const notificationCount = this.props.notificationCount;
|
||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||
|
||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
|
||||
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
|
||||
const notifBadges = notificationCount > 0 && RoomNotifs.shouldShowNotifBadge(this.state.notifState);
|
||||
const mentionBadges = this.props.highlight && RoomNotifs.shouldShowMentionBadge(this.state.notifState);
|
||||
const badges = notifBadges || mentionBadges;
|
||||
|
||||
let subtext = null;
|
||||
|
|
|
@ -97,6 +97,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="mx_RoomUpgradeWarningBar">
|
||||
<div className="mx_RoomUpgradeWarningBar_wrapped">
|
||||
<div className="mx_RoomUpgradeWarningBar_header">
|
||||
{_t(
|
||||
"This room is running room version <roomVersion />, which this homeserver has " +
|
||||
|
@ -113,6 +114,7 @@ module.exports = React.createClass({
|
|||
{_t("Only room administrators will see this warning")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
@ -53,6 +52,9 @@ export default class Stickerpicker extends React.Component {
|
|||
this.popoverWidth = 300;
|
||||
this.popoverHeight = 300;
|
||||
|
||||
// This is loaded by _acquireScalarClient on an as-needed basis.
|
||||
this.scalarClient = null;
|
||||
|
||||
this.state = {
|
||||
showStickers: false,
|
||||
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');
|
||||
if (this.state.widgetId) {
|
||||
this.scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
|
||||
if (scalarClient) {
|
||||
scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
|
||||
console.warn('Assets disabled');
|
||||
}).catch((err) => {
|
||||
console.error('Failed to disable assets');
|
||||
});
|
||||
} else {
|
||||
console.error("Cannot disable assets: no scalar client");
|
||||
}
|
||||
} else {
|
||||
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
|
||||
window.addEventListener('resize', this._onResize);
|
||||
|
||||
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) => {
|
||||
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
|
||||
MatrixClientPeg.get().on('accountData', this._updateWidget);
|
||||
|
@ -126,7 +136,7 @@ export default class Stickerpicker extends React.Component {
|
|||
console.error(errorMsg, e);
|
||||
this.setState({
|
||||
showStickers: false,
|
||||
imError: errorMsg,
|
||||
imError: _t(errorMsg),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -339,22 +349,13 @@ export default class Stickerpicker extends React.Component {
|
|||
*/
|
||||
_launchManageIntegrations() {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
this.scalarClient.connect().done(() => {
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room,
|
||||
'type_' + widgetType,
|
||||
this.state.widgetId,
|
||||
) :
|
||||
null;
|
||||
|
||||
// The integrations manager will handle scalar auth for us.
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
room: this.props.room,
|
||||
screen: `type_${widgetType}`,
|
||||
integrationId: this.state.widgetId,
|
||||
}, "mx_IntegrationsManager");
|
||||
this.setState({showStickers: false});
|
||||
}, (err) => {
|
||||
this.setState({imError: err});
|
||||
console.error('Error ensuring a valid scalar_token exists', err);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -14,50 +15,124 @@ See the License for the specific language governing permissions and
|
|||
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');
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const dis = require('../../../dispatcher');
|
||||
export default class IntegrationsManager extends React.Component {
|
||||
static propTypes = {
|
||||
// the room object where the integrations manager should be opened in
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'IntegrationsManager',
|
||||
// the screen name to open
|
||||
screen: PropTypes.string,
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired, // the source of the integration manager being embedded
|
||||
onFinished: React.PropTypes.func.isRequired, // callback when the lightbox is dismissed
|
||||
},
|
||||
// the integration ID to open
|
||||
integrationId: PropTypes.string,
|
||||
|
||||
// XXX: keyboard shortcuts for managing dialogs should be done by the modal
|
||||
// dialog base class somehow, surely...
|
||||
componentDidMount: function() {
|
||||
// callback when the manager is dismissed
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
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);
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
}
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
if (ev.keyCode == 27) { // escape
|
||||
onKeyDown = (ev) => {
|
||||
if (ev.keyCode === 27) { // escape
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onAction: function(payload) {
|
||||
onAction = (payload) => {
|
||||
if (payload.action === 'close_scalar') {
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
if (!this.state.configured) {
|
||||
return (
|
||||
<iframe src={ this.props.src }></iframe>
|
||||
<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();
|
||||
|
||||
this.state = {
|
||||
mediaDevices: null,
|
||||
mediaDevices: false,
|
||||
activeAudioOutput: null,
|
||||
activeAudioInput: null,
|
||||
activeVideoInput: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(): void {
|
||||
async componentDidMount() {
|
||||
const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
|
||||
if (canSeeDeviceLabels) {
|
||||
this._refreshMediaDevices();
|
||||
}
|
||||
}
|
||||
|
||||
_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({
|
||||
mediaDevices: await CallMediaHandler.getDevices(),
|
||||
activeAudioOutput: CallMediaHandler.getAudioOutput(),
|
||||
activeAudioInput: CallMediaHandler.getAudioInput(),
|
||||
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 = () => {
|
||||
const getUserMedia = (
|
||||
window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia
|
||||
);
|
||||
if (getUserMedia) {
|
||||
return getUserMedia.apply(window.navigator, [
|
||||
{ video: true, audio: true },
|
||||
this._refreshMediaDevices,
|
||||
function() {
|
||||
_requestMediaPermissions = async () => {
|
||||
let constraints;
|
||||
let stream;
|
||||
let error;
|
||||
try {
|
||||
constraints = {video: true, audio: true};
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
} catch (err) {
|
||||
// user likely doesn't have a webcam,
|
||||
// we should still allow to select a microphone
|
||||
if (err.name === "NotFoundError") {
|
||||
constraints = { audio: true };
|
||||
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) {
|
||||
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() {
|
||||
|
|
|
@ -31,7 +31,7 @@ export default class VerificationCancelled extends React.Component {
|
|||
"The other party cancelled the verification.",
|
||||
)}</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t('Cancel')}
|
||||
primaryButton={_t('OK')}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.props.onDone}
|
||||
/>
|
||||
|
|
|
@ -30,12 +30,15 @@ import {getAddressType} from "./UserAddress";
|
|||
* @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 {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
|
||||
* action was aborted or failed.
|
||||
*/
|
||||
function createRoom(opts) {
|
||||
opts = opts || {};
|
||||
if (opts.spinner === undefined) opts.spinner = true;
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
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;
|
||||
return client.createRoom(createOpts).finally(function() {
|
||||
modal.close();
|
||||
if (modal) modal.close();
|
||||
}).then(function(res) {
|
||||
roomId = res.room_id;
|
||||
if (opts.dmUserId) {
|
||||
|
|
|
@ -15,22 +15,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {UserPillPart, RoomPillPart, PlainPart} from "./parts";
|
||||
|
||||
export default class AutocompleteWrapperModel {
|
||||
constructor(updateCallback, getAutocompleterComponent, updateQuery, room, client) {
|
||||
constructor(updateCallback, getAutocompleterComponent, updateQuery, partCreator) {
|
||||
this._updateCallback = updateCallback;
|
||||
this._getAutocompleterComponent = getAutocompleterComponent;
|
||||
this._updateQuery = updateQuery;
|
||||
this._partCreator = partCreator;
|
||||
this._query = null;
|
||||
this._room = room;
|
||||
this._client = client;
|
||||
}
|
||||
|
||||
onEscape(e) {
|
||||
this._getAutocompleterComponent().onEscape(e);
|
||||
this._updateCallback({
|
||||
replacePart: new PlainPart(this._queryPart.text),
|
||||
replacePart: this._partCreator.plain(this._queryPart.text),
|
||||
caretOffset: this._queryOffset,
|
||||
close: true,
|
||||
});
|
||||
|
@ -93,21 +90,22 @@ export default class AutocompleteWrapperModel {
|
|||
}
|
||||
|
||||
_partForCompletion(completion) {
|
||||
const firstChr = completion.completionId && completion.completionId[0];
|
||||
const {completionId} = completion;
|
||||
const text = completion.completion;
|
||||
const firstChr = completionId && completionId[0];
|
||||
switch (firstChr) {
|
||||
case "@": {
|
||||
const displayName = completion.completion;
|
||||
const userId = completion.completionId;
|
||||
const member = this._room.getMember(userId);
|
||||
return new UserPillPart(userId, displayName, member);
|
||||
if (completionId === "@room") {
|
||||
return this._partCreator.atRoomPill(completionId);
|
||||
} else {
|
||||
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
|
||||
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.
|
||||
*/
|
||||
|
||||
import {needsCaretNodeBefore, needsCaretNodeAfter} from "./render";
|
||||
|
||||
export function setCaretPosition(editor, model, caretPosition) {
|
||||
const sel = document.getSelection();
|
||||
sel.removeAllRanges();
|
||||
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 {index} = caretPosition;
|
||||
const partIndex = caretPosition.index;
|
||||
const lineResult = findNodeInLineForPart(parts, partIndex);
|
||||
const {lineIndex} = lineResult;
|
||||
let {nodeIndex} = lineResult;
|
||||
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 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];
|
||||
if (part && part.type === "newline") {
|
||||
if (i < index) {
|
||||
if (part.type === "newline") {
|
||||
lineIndex += 1;
|
||||
nodeIndex = -1;
|
||||
prevPart = null;
|
||||
} else {
|
||||
// if index points at a newline part,
|
||||
// put the caret at the end of the previous part
|
||||
// so it stays on the same line
|
||||
const prevPart = parts[i - 1];
|
||||
offset = prevPart ? prevPart.text.length : 0;
|
||||
nodeIndex += 1;
|
||||
if (needsCaretNodeBefore(part, prevPart)) {
|
||||
nodeIndex += 1;
|
||||
}
|
||||
} else {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
let focusNode;
|
||||
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];
|
||||
prevPart = part;
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
|
||||
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 {
|
||||
nodeIndex += 1;
|
||||
offset = 0;
|
||||
}
|
||||
}
|
||||
return {nodeIndex, offset};
|
||||
}
|
||||
|
|
|
@ -16,73 +16,92 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
|
||||
import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts";
|
||||
import { walkDOMDepthFirst } from "./dom";
|
||||
|
||||
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 pillMatch = REGEX_MATRIXTO.exec(href) || [];
|
||||
const resourceId = pillMatch[1]; // The room/user ID
|
||||
const prefix = pillMatch[2]; // The first character of prefix
|
||||
switch (prefix) {
|
||||
case "@":
|
||||
return new UserPillPart(
|
||||
resourceId,
|
||||
a.textContent,
|
||||
room.getMember(resourceId),
|
||||
);
|
||||
return partCreator.userPill(a.textContent, resourceId);
|
||||
case "#":
|
||||
return new RoomPillPart(resourceId, client);
|
||||
return partCreator.roomPill(resourceId);
|
||||
default: {
|
||||
if (href === a.textContent) {
|
||||
return new PlainPart(a.textContent);
|
||||
return partCreator.plain(a.textContent);
|
||||
} else {
|
||||
return new PlainPart(`[${a.textContent}](${href})`);
|
||||
return partCreator.plain(`[${a.textContent}](${href})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseCodeBlock(n) {
|
||||
function parseCodeBlock(n, partCreator) {
|
||||
const parts = [];
|
||||
const preLines = ("```\n" + n.textContent + "```").split("\n");
|
||||
preLines.forEach((l, i) => {
|
||||
parts.push(new PlainPart(l));
|
||||
parts.push(partCreator.plain(l));
|
||||
if (i < preLines.length - 1) {
|
||||
parts.push(new NewlinePart("\n"));
|
||||
parts.push(partCreator.newline());
|
||||
}
|
||||
});
|
||||
return parts;
|
||||
}
|
||||
|
||||
function parseElement(n, room, client) {
|
||||
function parseElement(n, partCreator, state) {
|
||||
switch (n.nodeName) {
|
||||
case "A":
|
||||
return parseLink(n, room, client);
|
||||
return parseLink(n, partCreator);
|
||||
case "BR":
|
||||
return new NewlinePart("\n");
|
||||
return partCreator.newline();
|
||||
case "EM":
|
||||
return new PlainPart(`*${n.textContent}*`);
|
||||
return partCreator.plain(`*${n.textContent}*`);
|
||||
case "STRONG":
|
||||
return new PlainPart(`**${n.textContent}**`);
|
||||
return partCreator.plain(`**${n.textContent}**`);
|
||||
case "PRE":
|
||||
return parseCodeBlock(n);
|
||||
return parseCodeBlock(n, partCreator);
|
||||
case "CODE":
|
||||
return new PlainPart(`\`${n.textContent}\``);
|
||||
return partCreator.plain(`\`${n.textContent}\``);
|
||||
case "DEL":
|
||||
return new PlainPart(`<del>${n.textContent}</del>`);
|
||||
case "LI":
|
||||
return partCreator.plain(`<del>${n.textContent}</del>`);
|
||||
case "LI": {
|
||||
const indent = " ".repeat(state.listDepth - 1);
|
||||
if (n.parentElement.nodeName === "OL") {
|
||||
return new PlainPart(` 1. `);
|
||||
return partCreator.plain(`${indent}1. `);
|
||||
} 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:
|
||||
// don't textify block nodes we'll decend into
|
||||
if (!checkDecendInto(n)) {
|
||||
return new PlainPart(n.textContent);
|
||||
return partCreator.plain(n.textContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,22 +144,22 @@ function checkIgnored(n) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function prefixQuoteLines(isFirstNode, parts) {
|
||||
function prefixQuoteLines(isFirstNode, parts, partCreator) {
|
||||
const PREFIX = "> ";
|
||||
// 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 (isFirstNode) {
|
||||
parts.splice(0, 0, new PlainPart(PREFIX));
|
||||
parts.splice(0, 0, partCreator.plain(PREFIX));
|
||||
}
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
if (parts[i].type === "newline") {
|
||||
parts.splice(i + 1, 0, new PlainPart(PREFIX));
|
||||
parts.splice(i + 1, 0, partCreator.plain(PREFIX));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseHtmlMessage(html, room, client) {
|
||||
function parseHtmlMessage(html, partCreator) {
|
||||
// no nodes from parsing here should be inserted in the document,
|
||||
// as scripts in event handlers, etc would be executed then.
|
||||
// we're only taking text, so that is fine
|
||||
|
@ -148,6 +167,7 @@ function parseHtmlMessage(html, room, client) {
|
|||
const parts = [];
|
||||
let lastNode;
|
||||
let inQuote = false;
|
||||
const state = {};
|
||||
|
||||
function onNodeEnter(n) {
|
||||
if (checkIgnored(n)) {
|
||||
|
@ -159,13 +179,13 @@ function parseHtmlMessage(html, room, client) {
|
|||
|
||||
const newParts = [];
|
||||
if (lastNode && (checkBlockNode(lastNode) || checkBlockNode(n))) {
|
||||
newParts.push(new NewlinePart("\n"));
|
||||
newParts.push(partCreator.newline());
|
||||
}
|
||||
|
||||
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) {
|
||||
const parseResult = parseElement(n, room, client);
|
||||
const parseResult = parseElement(n, partCreator, state);
|
||||
if (parseResult) {
|
||||
if (Array.isArray(parseResult)) {
|
||||
newParts.push(...parseResult);
|
||||
|
@ -177,14 +197,14 @@ function parseHtmlMessage(html, room, client) {
|
|||
|
||||
if (newParts.length && inQuote) {
|
||||
const isFirstPart = parts.length === 0;
|
||||
prefixQuoteLines(isFirstPart, newParts);
|
||||
prefixQuoteLines(isFirstPart, newParts, partCreator);
|
||||
}
|
||||
|
||||
parts.push(...newParts);
|
||||
|
||||
// extra newline after quote, only if there something behind it...
|
||||
if (lastNode && lastNode.nodeName === "BLOCKQUOTE") {
|
||||
parts.push(new NewlinePart("\n"));
|
||||
parts.push(partCreator.newline());
|
||||
}
|
||||
lastNode = null;
|
||||
return checkDecendInto(n);
|
||||
|
@ -194,8 +214,14 @@ function parseHtmlMessage(html, room, client) {
|
|||
if (checkIgnored(n)) {
|
||||
return;
|
||||
}
|
||||
if (n.nodeName === "BLOCKQUOTE") {
|
||||
switch (n.nodeName) {
|
||||
case "BLOCKQUOTE":
|
||||
inQuote = false;
|
||||
break;
|
||||
case "OL":
|
||||
case "UL":
|
||||
state.listDepth -= 1;
|
||||
break;
|
||||
}
|
||||
lastNode = n;
|
||||
}
|
||||
|
@ -205,23 +231,25 @@ function parseHtmlMessage(html, room, client) {
|
|||
return parts;
|
||||
}
|
||||
|
||||
export function parseEvent(event, room, client) {
|
||||
export function parseEvent(event, partCreator) {
|
||||
const content = event.getContent();
|
||||
let parts;
|
||||
if (content.format === "org.matrix.custom.html") {
|
||||
return parseHtmlMessage(content.formatted_body || "", room, client);
|
||||
parts = parseHtmlMessage(content.formatted_body || "", partCreator);
|
||||
} else {
|
||||
const body = content.body || "";
|
||||
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 text = new PlainPart(line);
|
||||
const newLine = !isLast && new NewlinePart("\n");
|
||||
if (newLine) {
|
||||
return parts.concat(text, newLine);
|
||||
} else {
|
||||
return parts.concat(text);
|
||||
const newParts = parseAtRoomMentions(line, partCreator);
|
||||
if (!isLast) {
|
||||
newParts.push(partCreator.newline());
|
||||
}
|
||||
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.
|
||||
*/
|
||||
|
||||
import {CARET_NODE_CHAR, isCaretNode} from "./render";
|
||||
|
||||
export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) {
|
||||
let node = rootNode.firstChild;
|
||||
while (node && node !== rootNode) {
|
||||
|
@ -38,27 +40,54 @@ export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback
|
|||
}
|
||||
|
||||
export function getCaretOffsetAndText(editor, sel) {
|
||||
let {focusNode} = sel;
|
||||
const {focusOffset} = sel;
|
||||
let caretOffset = focusOffset;
|
||||
let {focusNode, focusOffset} = sel;
|
||||
// sometimes focusNode is an element, and then focusOffset means
|
||||
// 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 text = "";
|
||||
|
||||
if (focusNode.nodeType === Node.ELEMENT_NODE && focusOffset !== 0) {
|
||||
focusNode = focusNode.childNodes[focusOffset - 1];
|
||||
caretOffset = focusNode.textContent.length;
|
||||
}
|
||||
|
||||
function enterNodeCallback(node) {
|
||||
const nodeText = node.nodeType === Node.TEXT_NODE && node.nodeValue;
|
||||
if (!foundCaret) {
|
||||
if (node === focusNode) {
|
||||
foundCaret = true;
|
||||
}
|
||||
}
|
||||
const nodeText = node.nodeType === Node.TEXT_NODE && getTextNodeValue(node);
|
||||
if (nodeText) {
|
||||
if (!foundCaret) {
|
||||
caretOffset += nodeText.length;
|
||||
focusNodeOffset += nodeText.length;
|
||||
}
|
||||
text += nodeText;
|
||||
}
|
||||
|
@ -73,14 +102,30 @@ export function getCaretOffsetAndText(editor, sel) {
|
|||
if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") {
|
||||
text += "\n";
|
||||
if (!foundCaret) {
|
||||
caretOffset += 1;
|
||||
focusNodeOffset += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback);
|
||||
|
||||
const atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length;
|
||||
const caret = {atNodeEnd, offset: caretOffset};
|
||||
return {caret, text};
|
||||
return {text, focusNodeOffset};
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new EditorModel(this._parts, this._partCreator, this._updateCallback);
|
||||
}
|
||||
|
||||
_insertPart(index, part) {
|
||||
this._parts.splice(index, 0, part);
|
||||
if (this._activePartIdx >= index) {
|
||||
|
@ -91,7 +95,7 @@ export default class EditorModel {
|
|||
const position = this.positionForOffset(diff.at, caret.atNodeEnd);
|
||||
let removedOffsetDecrease = 0;
|
||||
if (diff.removed) {
|
||||
removedOffsetDecrease = this._removeText(position, diff.removed.length);
|
||||
removedOffsetDecrease = this.removeText(position, diff.removed.length);
|
||||
}
|
||||
let addedLen = 0;
|
||||
if (diff.added) {
|
||||
|
@ -99,8 +103,7 @@ export default class EditorModel {
|
|||
}
|
||||
this._mergeAdjacentParts();
|
||||
const caretOffset = diff.at - removedOffsetDecrease + addedLen;
|
||||
let newPosition = this.positionForOffset(caretOffset, true);
|
||||
newPosition = newPosition.skipUneditableParts(this._parts);
|
||||
const newPosition = this.positionForOffset(caretOffset, true);
|
||||
this._setActivePart(newPosition);
|
||||
this._updateCallback(newPosition);
|
||||
}
|
||||
|
@ -136,10 +139,9 @@ export default class EditorModel {
|
|||
let pos;
|
||||
if (replacePart) {
|
||||
this._replacePart(this._autoCompletePartIdx, replacePart);
|
||||
let index = this._autoCompletePartIdx;
|
||||
const index = this._autoCompletePartIdx;
|
||||
if (caretOffset === undefined) {
|
||||
caretOffset = 0;
|
||||
index += 1;
|
||||
caretOffset = replacePart.text.length;
|
||||
}
|
||||
pos = new DocumentPosition(index, caretOffset);
|
||||
}
|
||||
|
@ -154,11 +156,11 @@ export default class EditorModel {
|
|||
}
|
||||
|
||||
_mergeAdjacentParts(docPos) {
|
||||
let prevPart = this._parts[0];
|
||||
for (let i = 1; i < this._parts.length; ++i) {
|
||||
let prevPart;
|
||||
for (let i = 0; i < this._parts.length; ++i) {
|
||||
let part = this._parts[i];
|
||||
const isEmpty = !part.text.length;
|
||||
const isMerged = !isEmpty && prevPart.merge(part);
|
||||
const isMerged = !isEmpty && prevPart && prevPart.merge(part);
|
||||
if (isEmpty || isMerged) {
|
||||
// remove empty or merged part
|
||||
part = prevPart;
|
||||
|
@ -177,7 +179,7 @@ export default class EditorModel {
|
|||
* @return {Number} how many characters before pos were also removed,
|
||||
* 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 removedOffsetDecrease = 0;
|
||||
while (len > 0) {
|
||||
|
@ -279,13 +281,4 @@ class DocumentPosition {
|
|||
get 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) {
|
||||
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) {
|
||||
return (this.text.length + i) === 0 && chr === "\n";
|
||||
}
|
||||
|
@ -235,20 +235,10 @@ export class NewlinePart extends BasePart {
|
|||
}
|
||||
}
|
||||
|
||||
export class RoomPillPart extends PillPart {
|
||||
constructor(displayAlias, client) {
|
||||
class RoomPillPart extends PillPart {
|
||||
constructor(displayAlias, room) {
|
||||
super(displayAlias, displayAlias);
|
||||
this._room = this._findRoomByAlias(displayAlias, client);
|
||||
}
|
||||
|
||||
_findRoomByAlias(alias, client) {
|
||||
if (alias[0] === '#') {
|
||||
return client.getRooms().find((r) => {
|
||||
return r.getAliases().includes(alias);
|
||||
});
|
||||
} else {
|
||||
return client.getRoom(alias);
|
||||
}
|
||||
this._room = room;
|
||||
}
|
||||
|
||||
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) {
|
||||
super(userId, displayName);
|
||||
this._member = member;
|
||||
|
@ -311,7 +307,7 @@ export class UserPillPart extends PillPart {
|
|||
}
|
||||
|
||||
|
||||
export class PillCandidatePart extends PlainPart {
|
||||
class PillCandidatePart extends PlainPart {
|
||||
constructor(text, autoCompleteCreator) {
|
||||
super(text);
|
||||
this._autoCompleteCreator = autoCompleteCreator;
|
||||
|
@ -351,8 +347,7 @@ export class PartCreator {
|
|||
updateCallback,
|
||||
getAutocompleterComponent,
|
||||
updateQuery,
|
||||
room,
|
||||
client,
|
||||
this,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -362,7 +357,7 @@ export class PartCreator {
|
|||
case "#":
|
||||
case "@":
|
||||
case ":":
|
||||
return new PillCandidatePart("", this._autoCompleteCreator);
|
||||
return this.pillCandidate("");
|
||||
case "\n":
|
||||
return new NewlinePart();
|
||||
default:
|
||||
|
@ -371,24 +366,57 @@ export class PartCreator {
|
|||
}
|
||||
|
||||
createDefaultPart(text) {
|
||||
return new PlainPart(text);
|
||||
return this.plain(text);
|
||||
}
|
||||
|
||||
deserializePart(part) {
|
||||
switch (part.type) {
|
||||
case "plain":
|
||||
return new PlainPart(part.text);
|
||||
return this.plain(part.text);
|
||||
case "newline":
|
||||
return new NewlinePart(part.text);
|
||||
return this.newline();
|
||||
case "at-room-pill":
|
||||
return this.atRoomPill(part.text);
|
||||
case "pill-candidate":
|
||||
return new PillCandidatePart(part.text, this._autoCompleteCreator);
|
||||
return this.pillCandidate(part.text);
|
||||
case "room-pill":
|
||||
return new RoomPillPart(part.text, this._client);
|
||||
case "user-pill": {
|
||||
const member = this._room.getMember(part.userId);
|
||||
return new UserPillPart(part.userId, part.text, member);
|
||||
return this.roomPill(part.text);
|
||||
case "user-pill":
|
||||
return this.userPill(part.text, part.userId);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
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) {
|
||||
const lines = model.parts.reduce((lines, part) => {
|
||||
if (part.type === "newline") {
|
||||
|
@ -25,8 +156,9 @@ export function renderModel(editor, model) {
|
|||
}
|
||||
return lines;
|
||||
}, [[]]);
|
||||
// TODO: refactor this code, DRY it
|
||||
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];
|
||||
while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) {
|
||||
editor.removeChild(lineContainer);
|
||||
|
@ -38,46 +170,14 @@ export function renderModel(editor, model) {
|
|||
}
|
||||
|
||||
if (parts.length) {
|
||||
parts.forEach((part, j) => {
|
||||
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());
|
||||
reconcileLine(lineContainer, parts);
|
||||
} else {
|
||||
reconcileEmptyLine(lineContainer);
|
||||
}
|
||||
});
|
||||
|
||||
let surplusElementCount = Math.max(0, lineContainer.childNodes.length - parts.length);
|
||||
while (surplusElementCount) {
|
||||
lineContainer.removeChild(lineContainer.lastChild);
|
||||
--surplusElementCount;
|
||||
}
|
||||
if (lines.length) {
|
||||
removeNextSiblings(editor.children[lines.length]);
|
||||
} else {
|
||||
// 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 {
|
||||
lineContainer.removeChild(partNode);
|
||||
removeChildren(editor);
|
||||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ export function mdSerialize(model) {
|
|||
return html + "\n";
|
||||
case "plain":
|
||||
case "pill-candidate":
|
||||
case "at-room-pill":
|
||||
return html + part.text;
|
||||
case "room-pill":
|
||||
case "user-pill":
|
||||
|
@ -47,6 +48,7 @@ export function textSerialize(model) {
|
|||
return text + "\n";
|
||||
case "plain":
|
||||
case "pill-candidate":
|
||||
case "at-room-pill":
|
||||
return text + part.text;
|
||||
case "room-pill":
|
||||
case "user-pill":
|
||||
|
@ -58,13 +60,11 @@ export function textSerialize(model) {
|
|||
export function requiresHtml(model) {
|
||||
return model.parts.some(part => {
|
||||
switch (part.type) {
|
||||
case "newline":
|
||||
case "plain":
|
||||
case "pill-candidate":
|
||||
return false;
|
||||
case "room-pill":
|
||||
case "user-pill":
|
||||
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