mirror of
https://github.com/element-hq/element-web
synced 2024-11-26 19:26:04 +03:00
Merge branch 'develop' into 19245-improve-styling-of-search-initialization-errors
This commit is contained in:
commit
401e124df6
90 changed files with 562 additions and 398 deletions
|
@ -1,6 +1,7 @@
|
|||
[![npm](https://img.shields.io/npm/v/matrix-react-sdk)](https://www.npmjs.com/package/matrix-react-sdk)
|
||||
![Tests](https://github.com/matrix-org/matrix-react-sdk/actions/workflows/tests.yml/badge.svg)
|
||||
![Static Analysis](https://github.com/matrix-org/matrix-react-sdk/actions/workflows/static_analysis.yaml/badge.svg)
|
||||
[![matrix-react-sdk](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ppvnzg/develop&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/ppvnzg/runs)
|
||||
[![Weblate](https://translate.element.io/widgets/element-web/-/matrix-react-sdk/svg-badge.svg)](https://translate.element.io/engage/element-web/)
|
||||
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
|
||||
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=coverage)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
"linkifyjs": "4.0.0-beta.4",
|
||||
"lodash": "^4.17.20",
|
||||
"maplibre-gl": "^1.15.2",
|
||||
"matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#4aef17b56798639906f26a8739043a3c5c5fde7e",
|
||||
"matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#a0687ca6fbdb7258543d49b99fb88b9201e900b0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
|
|
|
@ -385,15 +385,22 @@ legend {
|
|||
color: $alert;
|
||||
}
|
||||
|
||||
.mx_Dialog_cancelButton {
|
||||
@define-mixin customisedCancelButton {
|
||||
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: $dialog-close-fg-color;
|
||||
cursor: pointer;
|
||||
position: unset;
|
||||
width: unset;
|
||||
height: unset;
|
||||
}
|
||||
|
||||
.mx_Dialog_cancelButton {
|
||||
@mixin customisedCancelButton;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 0;
|
||||
|
|
|
@ -34,14 +34,9 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_CompleteSecurity_skip {
|
||||
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
@mixin customisedCancelButton;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: $dialog-close-fg-color;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ limitations under the License.
|
|||
font-size: 12px;
|
||||
font-weight: $font-semi-bold;
|
||||
line-height: 15px;
|
||||
color: #FFFFFF;
|
||||
color: $button-primary-fg-color;
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
word-break: keep-all; // avoid multiple lines on CJK language
|
||||
|
|
|
@ -38,14 +38,9 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_CompoundDialog_cancelButton {
|
||||
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
@mixin customisedCancelButton;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: $dialog-close-fg-color;
|
||||
cursor: pointer;
|
||||
|
||||
// Align with middle of title, 34px from right edge
|
||||
position: absolute;
|
||||
|
|
|
@ -25,14 +25,12 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_EditableItem_delete {
|
||||
@mixin customisedCancelButton;
|
||||
order: 3;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
mask-image: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
background-color: $alert;
|
||||
mask-size: 100%;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ limitations under the License.
|
|||
flex: 1 1 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid $menu-selected-color;
|
||||
}
|
||||
|
||||
.mx_DateSeparator > div {
|
||||
|
|
|
@ -38,3 +38,7 @@ limitations under the License.
|
|||
max-width: 100%;
|
||||
width: 450px;
|
||||
}
|
||||
|
||||
.mx_DisambiguatedProfile ~ .mx_MLocationBody {
|
||||
margin-top: 6px; // See: https://github.com/matrix-org/matrix-react-sdk/pull/8442
|
||||
}
|
||||
|
|
|
@ -25,14 +25,10 @@ limitations under the License.
|
|||
|
||||
.mx_UserInfo {
|
||||
.mx_EncryptionPanel_cancel {
|
||||
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
@mixin customisedCancelButton;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: $settings-subsection-fg-color;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 14px;
|
||||
|
|
|
@ -50,6 +50,12 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
|
|||
.mx_EventTile_receiptSending::before {
|
||||
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
|
||||
}
|
||||
|
||||
&[data-layout=group] {
|
||||
.mx_EventTile_line {
|
||||
line-height: var(--GroupLayout-EventTile-line-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile:not([data-layout=bubble]) {
|
||||
|
@ -263,8 +269,15 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
|
|||
}
|
||||
}
|
||||
|
||||
.mx_GenericEventListSummary:not([data-layout=bubble]) .mx_EventTile_line {
|
||||
padding-left: $left-gutter;
|
||||
.mx_GenericEventListSummary:not([data-layout=bubble]) {
|
||||
.mx_EventTile_line {
|
||||
padding-left: $left-gutter;
|
||||
line-height: normal;
|
||||
|
||||
.mx_RedactedBody {
|
||||
line-height: 1; // remove spacing between lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line,
|
||||
|
@ -842,12 +855,6 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
|
|||
padding-right: 0;
|
||||
}
|
||||
|
||||
.mx_ReplyChain {
|
||||
.mx_MLocationBody {
|
||||
margin-top: 6px; // See: https://github.com/matrix-org/matrix-react-sdk/pull/8442
|
||||
}
|
||||
}
|
||||
|
||||
&:not([data-layout=bubble]) {
|
||||
padding-top: $spacing-16;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
|||
$left-gutter: 64px;
|
||||
|
||||
.mx_GroupLayout {
|
||||
--GroupLayout-EventTile-line-height: $font-22px;
|
||||
|
||||
.mx_EventTile {
|
||||
> .mx_DisambiguatedProfile {
|
||||
line-height: $font-20px;
|
||||
|
@ -33,10 +35,14 @@ $left-gutter: 64px;
|
|||
position: absolute; // for modern layout
|
||||
}
|
||||
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
.mx_EventTile_line,
|
||||
.mx_EventTile_reply {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 3px;
|
||||
line-height: $font-22px;
|
||||
}
|
||||
|
||||
.mx_EventTile_reply {
|
||||
line-height: var(--GroupLayout-EventTile-line-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +53,8 @@ $left-gutter: 64px;
|
|||
.mx_EventTile {
|
||||
padding-top: 4px;
|
||||
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
.mx_EventTile_line,
|
||||
.mx_EventTile_reply {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
@ -56,9 +63,12 @@ $left-gutter: 64px;
|
|||
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
|
||||
padding-top: 0px;
|
||||
font-size: $font-13px;
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
|
||||
.mx_EventTile_line,
|
||||
.mx_EventTile_reply {
|
||||
line-height: $font-20px;
|
||||
}
|
||||
|
||||
.mx_EventTile_avatar {
|
||||
top: 4px;
|
||||
}
|
||||
|
@ -71,10 +81,13 @@ $left-gutter: 64px;
|
|||
&.mx_EventTile_emote {
|
||||
// add a bit more space for emotes so that avatars don't collide
|
||||
padding-top: 8px;
|
||||
|
||||
.mx_EventTile_avatar {
|
||||
top: 2px;
|
||||
}
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
|
||||
.mx_EventTile_line,
|
||||
.mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
@ -82,7 +95,9 @@ $left-gutter: 64px;
|
|||
|
||||
&.mx_EventTile_emote.mx_EventTile_continuation {
|
||||
padding-top: 0;
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
|
||||
.mx_EventTile_line,
|
||||
.mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
|
|
@ -178,12 +178,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_ContextualMenu {
|
||||
.mx_MessageComposer_button {
|
||||
padding-left: calc(var(--size) + 6px);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageComposer_button {
|
||||
--size: 26px;
|
||||
position: relative;
|
||||
|
@ -192,20 +186,16 @@ limitations under the License.
|
|||
line-height: var(--size);
|
||||
width: auto;
|
||||
padding-left: var(--size);
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
|
||||
&:not(.mx_CallContextMenu_item) {
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
&:last-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
height: 20px;
|
||||
|
@ -399,18 +389,3 @@ limitations under the License.
|
|||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageComposer_Menu .mx_CallContextMenu_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: unset;
|
||||
margin: 7px 7px 7px 16px; // space out the buttons
|
||||
}
|
||||
|
||||
.mx_MessageComposer_Menu .mx_ContextualMenu {
|
||||
min-width: 150px;
|
||||
width: max-content;
|
||||
padding: 5px 10px 5px 0;
|
||||
box-shadow: 0px 2px 9px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
|
|
@ -16,12 +16,10 @@ limitations under the License.
|
|||
|
||||
.mx_ReplyPreview {
|
||||
border: 1px solid $primary-hairline-color;
|
||||
background: $background;
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
background: $background;
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
box-shadow: 0px -16px 32px $composer-shadow-color;
|
||||
|
||||
.mx_ReplyPreview_section {
|
||||
border-bottom: 1px solid $primary-hairline-color;
|
||||
|
@ -53,3 +51,12 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomView_body {
|
||||
.mx_ReplyPreview {
|
||||
// Add box-shadow to the reply preview on the main (left) panel only.
|
||||
// It is not added to the preview on the (right) panel for threads and a chat with a maximized widget.
|
||||
box-shadow: 0px -16px 32px $composer-shadow-color;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,10 @@ limitations under the License.
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mx_RoomTile_details {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mx_RoomTile_titleContainer {
|
||||
height: 32px;
|
||||
min-width: 0;
|
||||
|
|
|
@ -35,15 +35,10 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_DialPadContextMenu_cancel {
|
||||
@mixin customisedCancelButton;
|
||||
float: right;
|
||||
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: $dialog-close-fg-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_DialPadContextMenu_header:focus-within {
|
||||
|
|
|
@ -45,15 +45,10 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_DialPadModal_cancel {
|
||||
@mixin customisedCancelButton;
|
||||
float: right;
|
||||
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: $dialog-close-fg-color;
|
||||
cursor: pointer;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,11 @@
|
|||
* and it's better to rely on the browser's built-in obliquing behaviour.
|
||||
*/
|
||||
|
||||
// Grab the other fonts from the current theme, so we can override to Inter
|
||||
// in custom fonts if needed.
|
||||
@import "../../light/css/_fonts.scss";
|
||||
|
||||
// Nunito as the default, for old time's sake on legacy themes.
|
||||
/* the 'src' links are relative to the bundle.css, which is in a subdirectory.
|
||||
*/
|
||||
@font-face {
|
||||
|
@ -32,53 +37,3 @@
|
|||
font-weight: 700;
|
||||
src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* a COLR/CPAL version of Twemoji used for consistent cross-browser emoji
|
||||
* taken from https://github.com/mozilla/twemoji-colr
|
||||
* using the fix from https://github.com/mozilla/twemoji-colr/issues/50 to
|
||||
* work on macOS
|
||||
*/
|
||||
/*
|
||||
// except we now load it dynamically via FontManager to handle browsers
|
||||
// which can't render COLR/CPAL still
|
||||
@font-face {
|
||||
font-family: "Twemoji Mozilla";
|
||||
src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2');
|
||||
}
|
||||
*/
|
|
@ -237,7 +237,7 @@ export default abstract class BasePlatform {
|
|||
}
|
||||
|
||||
/**
|
||||
* Restarts the application, without neccessarily reloading
|
||||
* Restarts the application, without necessarily reloading
|
||||
* any application code
|
||||
*/
|
||||
abstract reload();
|
||||
|
|
|
@ -948,7 +948,7 @@ export default class CallHandler extends EventEmitter {
|
|||
): Promise<void> {
|
||||
if (consultFirst) {
|
||||
// if we're consulting, we just start by placing a call to the transfer
|
||||
// target (passing the transferee so the actual tranfer can happen later)
|
||||
// target (passing the transferee so the actual transfer can happen later)
|
||||
this.dialNumber(destination, call);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -187,7 +187,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
|
|||
delete attribs.target;
|
||||
}
|
||||
} else {
|
||||
// Delete the href attrib if it is falsey
|
||||
// Delete the href attrib if it is falsy
|
||||
delete attribs.href;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ limitations under the License.
|
|||
* reflect the actual height the scaled thumbnail occupies.
|
||||
*
|
||||
* This is very useful for calculating how much height a thumbnail will actually
|
||||
* consume in the timeline, when performing scroll offset calcuations
|
||||
* consume in the timeline, when performing scroll offset calculations
|
||||
* (e.g. scroll locking)
|
||||
*/
|
||||
export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
|
||||
|
|
|
@ -52,7 +52,7 @@ export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolea
|
|||
// Used to split rooms via tags
|
||||
const tagNames = Object.keys(room.tags);
|
||||
// Used for 1:1 direct chats
|
||||
// Show 1:1 chats in seperate "Direct Messages" section as long as they haven't
|
||||
// Show 1:1 chats in separate "Direct Messages" section as long as they haven't
|
||||
// been moved to a different tag section
|
||||
const totalMemberCount = room.currentState.getJoinedMemberCount() +
|
||||
room.currentState.getInvitedMemberCount();
|
||||
|
|
|
@ -291,7 +291,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
|
|||
changeText = _t("Use a different passphrase?");
|
||||
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
|
||||
// only tell them they're wrong if they've actually gone wrong.
|
||||
// Security concious readers will note that if you left element-web unattended
|
||||
// Security conscious readers will note that if you left element-web unattended
|
||||
// on this screen, this would make it easy for a malicious person to guess
|
||||
// your passphrase one letter at a time, but they could get this faster by
|
||||
// just opening the browser's developer tools and reading it.
|
||||
|
|
|
@ -649,7 +649,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
changeText = _t("Use a different passphrase?");
|
||||
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
|
||||
// only tell them they're wrong if they've actually gone wrong.
|
||||
// Security concious readers will note that if you left element-web unattended
|
||||
// Security conscious readers will note that if you left element-web unattended
|
||||
// on this screen, this would make it easy for a malicious person to guess
|
||||
// your passphrase one letter at a time, but they could get this faster by
|
||||
// just opening the browser's developer tools and reading it.
|
||||
|
|
|
@ -75,6 +75,8 @@ const UserWelcomeTop = () => {
|
|||
hasAvatarLabel={_tDom("Great, that'll help people know it's you")}
|
||||
noAvatarLabel={_tDom("Add a photo so people know it's you.")}
|
||||
setAvatarUrl={url => cli.setAvatarUrl(url)}
|
||||
isUserAvatar
|
||||
onClick={ev => PosthogTrackers.trackInteraction("WebHomeMiniAvatarUploadButton", ev)}
|
||||
>
|
||||
<BaseAvatar
|
||||
idName={userId}
|
||||
|
@ -100,7 +102,7 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
|
|||
}
|
||||
|
||||
let introSection;
|
||||
if (justRegistered) {
|
||||
if (justRegistered || !!OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) {
|
||||
introSection = <UserWelcomeTop />;
|
||||
} else {
|
||||
const brandingConfig = SdkConfig.getObject("branding");
|
||||
|
|
|
@ -135,7 +135,7 @@ export default class CountryDropdown extends React.Component<IProps, IState> {
|
|||
});
|
||||
|
||||
// default value here too, otherwise we need to handle null / undefined
|
||||
// values between mounting and the initial value propgating
|
||||
// values between mounting and the initial value propagating
|
||||
const value = this.props.value || this.state.defaultCountry.iso2;
|
||||
|
||||
return <Dropdown
|
||||
|
|
|
@ -56,7 +56,7 @@ const getLabel = (hasStoppingErrors: boolean, hasLocationErrors: boolean): strin
|
|||
return _t('An error occurred while stopping your live location');
|
||||
}
|
||||
if (hasLocationErrors) {
|
||||
return _t('An error occured whilst sharing your live location');
|
||||
return _t('An error occurred whilst sharing your live location');
|
||||
}
|
||||
return _t('You are sharing your live location');
|
||||
};
|
||||
|
@ -68,13 +68,13 @@ const useLivenessMonitor = (liveBeaconIds: BeaconIdentifier[], beacons: Map<Beac
|
|||
// refresh beacon monitors when the tab becomes active again
|
||||
const onPageVisibilityChanged = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
liveBeaconIds.map(identifier => beacons.get(identifier)?.monitorLiveness());
|
||||
liveBeaconIds.forEach(identifier => beacons.get(identifier)?.monitorLiveness());
|
||||
}
|
||||
};
|
||||
if (liveBeaconIds.length) {
|
||||
document.addEventListener("visibilitychange", onPageVisibilityChanged);
|
||||
}
|
||||
() => {
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", onPageVisibilityChanged);
|
||||
};
|
||||
}, [liveBeaconIds, beacons]);
|
||||
|
|
|
@ -29,7 +29,7 @@ import LiveTimeRemaining from './LiveTimeRemaining';
|
|||
|
||||
const getLabel = (hasLocationPublishError: boolean, hasStopSharingError: boolean): string => {
|
||||
if (hasLocationPublishError) {
|
||||
return _t('An error occured whilst sharing your live location, please try again');
|
||||
return _t('An error occurred whilst sharing your live location, please try again');
|
||||
}
|
||||
if (hasStopSharingError) {
|
||||
return _t('An error occurred while stopping your live location, please try again');
|
||||
|
|
|
@ -70,8 +70,8 @@ interface IProps extends IPosition {
|
|||
rightClick?: boolean;
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions?: Relations;
|
||||
// A permalink to the event
|
||||
showPermalink?: boolean;
|
||||
// A permalink to this event or an href of an anchor element the user has clicked
|
||||
link?: string;
|
||||
|
||||
getRelationsForEvent?: GetRelationsForEvent;
|
||||
}
|
||||
|
@ -227,7 +227,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
this.closeMenu();
|
||||
};
|
||||
|
||||
private onPermalinkClick = (e: React.MouseEvent): void => {
|
||||
private onShareClick = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
||||
target: this.props.mxEvent,
|
||||
|
@ -236,9 +236,9 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
this.closeMenu();
|
||||
};
|
||||
|
||||
private onCopyPermalinkClick = (e: ButtonEvent): void => {
|
||||
private onCopyLinkClick = (e: ButtonEvent): void => {
|
||||
e.preventDefault(); // So that we don't open the permalink
|
||||
copyPlaintext(this.getPermalink());
|
||||
copyPlaintext(this.props.link);
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
|
@ -295,11 +295,6 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
});
|
||||
}
|
||||
|
||||
private getPermalink(): string {
|
||||
if (!this.props.permalinkCreator) return;
|
||||
return this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||
}
|
||||
|
||||
private getUnsentReactions(): MatrixEvent[] {
|
||||
return this.getReactions(e => e.status === EventStatus.NOT_SENT);
|
||||
}
|
||||
|
@ -318,11 +313,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
public render(): JSX.Element {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const me = cli.getUserId();
|
||||
const { mxEvent, rightClick, showPermalink, eventTileOps, reactions, collapseReplyChain } = this.props;
|
||||
const { mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain } = this.props;
|
||||
const eventStatus = mxEvent.status;
|
||||
const unsentReactionsCount = this.getUnsentReactions().length;
|
||||
const contentActionable = isContentActionable(mxEvent);
|
||||
const permalink = this.getPermalink();
|
||||
const permalink = this.props.permalinkCreator?.forEvent(this.props.mxEvent.getId());
|
||||
// status is SENT before remote-echo, null after
|
||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
const { timelineRenderingType, canReact, canSendMessages } = this.context;
|
||||
|
@ -420,17 +415,13 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
if (permalink) {
|
||||
permalinkButton = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName={showPermalink
|
||||
? "mx_MessageContextMenu_iconCopy"
|
||||
: "mx_MessageContextMenu_iconPermalink"
|
||||
}
|
||||
onClick={showPermalink ? this.onCopyPermalinkClick : this.onPermalinkClick}
|
||||
label={showPermalink ? _t('Copy link') : _t('Share')}
|
||||
iconClassName="mx_MessageContextMenu_iconPermalink"
|
||||
onClick={this.onShareClick}
|
||||
label={_t('Share')}
|
||||
element="a"
|
||||
{
|
||||
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||
...{
|
||||
|
||||
href: permalink,
|
||||
target: "_blank",
|
||||
rel: "noreferrer noopener",
|
||||
|
@ -508,6 +499,26 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
);
|
||||
}
|
||||
|
||||
let copyLinkButton: JSX.Element;
|
||||
if (link) {
|
||||
copyLinkButton = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconCopy"
|
||||
onClick={this.onCopyLinkClick}
|
||||
label={_t('Copy link')}
|
||||
element="a"
|
||||
{
|
||||
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
|
||||
...{
|
||||
href: link,
|
||||
target: "_blank",
|
||||
rel: "noreferrer noopener",
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let copyButton: JSX.Element;
|
||||
if (rightClick && getSelectedText()) {
|
||||
copyButton = (
|
||||
|
@ -566,10 +577,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
let nativeItemsList: JSX.Element;
|
||||
if (copyButton) {
|
||||
if (copyButton || copyLinkButton) {
|
||||
nativeItemsList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{ copyButton }
|
||||
{ copyLinkButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import { _t } from '../../../languageHandler';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { IDialogProps } from "../dialogs/IDialogProps";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "../dialogs/BaseDialog";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
|
|
@ -263,7 +263,7 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
|
|||
else onFinished(false);
|
||||
};
|
||||
|
||||
const confirmCanel = async () => {
|
||||
const confirmCancel = async () => {
|
||||
await exporter?.cancelExport();
|
||||
setExportCancelled(true);
|
||||
setExporting(false);
|
||||
|
@ -346,7 +346,7 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
|
|||
hasCancel={true}
|
||||
cancelButton={_t("Continue")}
|
||||
onCancel={() => setCancelWarning(false)}
|
||||
onPrimaryButtonClick={confirmCanel}
|
||||
onPrimaryButtonClick={confirmCancel}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -29,7 +29,7 @@ interface IProps extends IDialogProps {
|
|||
error: string;
|
||||
}>>;
|
||||
source: string;
|
||||
continuation: () => void;
|
||||
continuation: () => Promise<void>;
|
||||
}
|
||||
|
||||
const KeySignatureUploadFailedDialog: React.FC<IProps> = ({
|
||||
|
|
|
@ -52,7 +52,7 @@ const socials = [
|
|||
}, {
|
||||
name: 'Reddit',
|
||||
img: require("../../../../res/img/social/reddit.png"),
|
||||
url: (url) => `http://www.reddit.com/submit?url=${url}`,
|
||||
url: (url) => `https://www.reddit.com/submit?url=${url}`,
|
||||
}, {
|
||||
name: 'email',
|
||||
img: require("../../../../res/img/social/email-1.png"),
|
||||
|
|
|
@ -44,7 +44,7 @@ enum ProgressState {
|
|||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
// if false, will close the dialog as soon as the restore completes succesfully
|
||||
// if false, will close the dialog as soon as the restore completes successfully
|
||||
// default: true
|
||||
showSummary?: boolean;
|
||||
// If specified, gather the key from the user but then call the function with the backup
|
||||
|
|
|
@ -96,7 +96,7 @@ export default function AccessibleButton({
|
|||
// that might receive focus as a result of the AccessibleButtonClick action
|
||||
// It's because we are using html buttons at a few places e.g. inside dialogs
|
||||
// And divs which we report as role button to assistive technologies.
|
||||
// Browsers handle space and enter keypresses differently and we are only adjusting to the
|
||||
// Browsers handle space and enter key presses differently and we are only adjusting to the
|
||||
// inconsistencies here
|
||||
newProps.onKeyDown = (e) => {
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(e);
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { SyntheticEvent } from 'react';
|
||||
import React, { SyntheticEvent, FocusEvent } from 'react';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Tooltip, { Alignment } from './Tooltip';
|
||||
|
@ -68,6 +68,12 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
|
|||
this.props.onHideTooltip?.(ev);
|
||||
};
|
||||
|
||||
private onFocus = (ev: FocusEvent) => {
|
||||
// We only show the tooltip if focus arrived here from some other
|
||||
// element, to avoid leaving tooltips hanging around when a modal closes
|
||||
if (ev.relatedTarget) this.showTooltip();
|
||||
};
|
||||
|
||||
render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, onHideTooltip,
|
||||
|
@ -84,7 +90,7 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
|
|||
{...props}
|
||||
onMouseOver={this.showTooltip}
|
||||
onMouseLeave={this.hideTooltip}
|
||||
onFocus={this.showTooltip}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.hideTooltip}
|
||||
aria-label={title}
|
||||
>
|
||||
|
|
|
@ -57,7 +57,7 @@ interface IProps {
|
|||
// which bypasses permission prompts as it was added explicitly by that user
|
||||
room?: Room;
|
||||
threadId?: string | null;
|
||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer container.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth?: boolean;
|
||||
// Optional. If set, renders a smaller view of the widget
|
||||
|
@ -288,7 +288,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
private setupSgListeners() {
|
||||
this.sgWidget.on("preparing", this.onWidgetPreparing);
|
||||
this.sgWidget.on("ready", this.onWidgetReady);
|
||||
// emits when the capabilites have been setup or changed
|
||||
// emits when the capabilities have been set up or changed
|
||||
this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified);
|
||||
}
|
||||
|
||||
|
@ -543,7 +543,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox " +
|
||||
"allow-same-origin allow-scripts allow-presentation allow-downloads";
|
||||
|
||||
// Additional iframe feature pemissions
|
||||
// Additional iframe feature permissions
|
||||
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
|
||||
const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write;";
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
|
|||
// in their own `console.error` invocation.
|
||||
logger.error(error);
|
||||
logger.error(
|
||||
"The above error occured while React was rendering the following components:",
|
||||
"The above error occurred while React was rendering the following components:",
|
||||
componentStack,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
|
|||
});
|
||||
|
||||
// default value here too, otherwise we need to handle null / undefined
|
||||
// values between mounting and the initial value propgating
|
||||
// values between mounting and the initial value propagating
|
||||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
let value = null;
|
||||
if (language) {
|
||||
|
|
|
@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext, useRef, useState } from 'react';
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import classNames from 'classnames';
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import React, { useContext, useRef, useState, MouseEvent } from 'react';
|
||||
|
||||
import Analytics from "../../../Analytics";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { useTimeout } from "../../../hooks/useTimeout";
|
||||
import { TranslatedString } from '../../../languageHandler';
|
||||
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Spinner from "./Spinner";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useTimeout } from "../../../hooks/useTimeout";
|
||||
import Analytics from "../../../Analytics";
|
||||
import { TranslatedString } from '../../../languageHandler';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
||||
|
||||
export const AVATAR_SIZE = 52;
|
||||
|
||||
|
@ -34,9 +34,13 @@ interface IProps {
|
|||
noAvatarLabel?: TranslatedString;
|
||||
hasAvatarLabel?: TranslatedString;
|
||||
setAvatarUrl(url: string): Promise<unknown>;
|
||||
isUserAvatar?: boolean;
|
||||
onClick?(ev: MouseEvent<HTMLInputElement>): void;
|
||||
}
|
||||
|
||||
const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => {
|
||||
const MiniAvatarUploader: React.FC<IProps> = ({
|
||||
hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, isUserAvatar, children, onClick,
|
||||
}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [hover, setHover] = useState(false);
|
||||
|
@ -54,7 +58,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
|
|||
const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel;
|
||||
|
||||
const { room } = useContext(RoomContext);
|
||||
const canSetAvatar = room?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId());
|
||||
const canSetAvatar = isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getUserId());
|
||||
if (!canSetAvatar) return <React.Fragment>{ children }</React.Fragment>;
|
||||
|
||||
const visible = !!label && (hover || show);
|
||||
|
@ -63,7 +67,10 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
|
|||
type="file"
|
||||
ref={uploadRef}
|
||||
className="mx_MiniAvatarUploader_input"
|
||||
onClick={chromeFileInputFix}
|
||||
onClick={(ev) => {
|
||||
chromeFileInputFix(ev);
|
||||
onClick?.(ev);
|
||||
}}
|
||||
onChange={async (ev) => {
|
||||
if (!ev.target.files?.length) return;
|
||||
setBusy(true);
|
||||
|
|
|
@ -167,7 +167,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
|
|||
await this.matrixClient.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
|
||||
} catch (e) {
|
||||
// if it fails catch the error and return early, there's no point trying to find the event in this case.
|
||||
// Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
|
||||
// Return null as it is falsy and thus should be treated as an error (as the event cannot be resolved).
|
||||
return null;
|
||||
}
|
||||
return this.room.findEventById(eventId);
|
||||
|
|
|
@ -99,7 +99,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
|
|||
});
|
||||
|
||||
// default value here too, otherwise we need to handle null / undefined;
|
||||
// values between mounting and the initial value propgating
|
||||
// values between mounting and the initial value propagating
|
||||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
let value = null;
|
||||
if (language) {
|
||||
|
|
|
@ -52,7 +52,7 @@ export default class TruncatedList extends React.Component<IProps> {
|
|||
return this.props.getChildren(start, end);
|
||||
} else {
|
||||
// XXX: I'm not sure why anything would pass null into this, it seems
|
||||
// like a bizzare case to handle, but I'm preserving the behaviour.
|
||||
// like a bizarre case to handle, but I'm preserving the behaviour.
|
||||
// (see commit 38d5c7d5c5d5a34dc16ef5d46278315f5c57f542)
|
||||
return React.Children.toArray(this.props.children).filter((c) => {
|
||||
return c != null;
|
||||
|
|
|
@ -58,7 +58,6 @@ export const LocationButton: React.FC<IProps> = ({ roomId, sender, menuPosition,
|
|||
|
||||
const className = classNames(
|
||||
"mx_MessageComposer_button",
|
||||
"mx_MessageComposer_location",
|
||||
{
|
||||
"mx_MessageComposer_button_highlight": menuDisplayed,
|
||||
},
|
||||
|
@ -67,6 +66,7 @@ export const LocationButton: React.FC<IProps> = ({ roomId, sender, menuPosition,
|
|||
return <React.Fragment>
|
||||
<CollapsibleButton
|
||||
className={className}
|
||||
iconClassName="mx_MessageComposer_location"
|
||||
onClick={openMenu}
|
||||
title={_t("Location")}
|
||||
/>
|
||||
|
|
|
@ -485,14 +485,14 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
return this.wrapImage(contentUrl, thumbnail);
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
// Overridden by MStickerBody
|
||||
protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
|
||||
return <a href={contentUrl} target={this.props.forExport ? "_blank" : undefined} onClick={this.onClick}>
|
||||
{ children }
|
||||
</a>;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
// Overridden by MStickerBody
|
||||
protected getPlaceholder(width: number, height: number): JSX.Element {
|
||||
const blurhash = this.props.mxEvent.getContent().info?.[BLURHASH_FIELD];
|
||||
|
||||
|
@ -506,12 +506,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
return <Spinner w={32} h={32} />;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
// Overridden by MStickerBody
|
||||
protected getTooltip(): JSX.Element {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
// Overridden by MStickerBody
|
||||
protected getFileBody(): string | JSX.Element {
|
||||
if (this.props.forExport) return null;
|
||||
/*
|
||||
|
|
|
@ -49,7 +49,7 @@ interface IState {
|
|||
// @ts-ignore - TS wants a string key, but we know better
|
||||
apps: {[id: Container]: IApp[]};
|
||||
resizingVertical: boolean; // true when changing the height of the apps drawer
|
||||
resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
|
||||
resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
|
||||
resizing: boolean;
|
||||
}
|
||||
|
||||
|
@ -259,7 +259,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
mx_AppsDrawer_2apps: apps.length === 2,
|
||||
mx_AppsDrawer_3apps: apps.length === 3,
|
||||
});
|
||||
const appConatiners =
|
||||
const appContainers =
|
||||
<div className="mx_AppsContainer" ref={this.collectResizer}>
|
||||
{ apps.map((app, i) => {
|
||||
if (i < 1) return app;
|
||||
|
@ -272,7 +272,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
|
||||
let drawer;
|
||||
if (widgetIsMaxmised) {
|
||||
drawer = appConatiners;
|
||||
drawer = appContainers;
|
||||
} else {
|
||||
drawer = <PersistentVResizer
|
||||
room={this.props.room}
|
||||
|
@ -282,7 +282,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
|
||||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}>
|
||||
{ appConatiners }
|
||||
{ appContainers }
|
||||
</PersistentVResizer>;
|
||||
}
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
const severity = ev.getContent().severity || "normal";
|
||||
const stateKey = ev.getStateKey();
|
||||
|
||||
// We want a non-empty title but can accept falsey values (e.g.
|
||||
// We want a non-empty title but can accept falsy values (e.g.
|
||||
// zero)
|
||||
if (title && value !== undefined) {
|
||||
counters.push({
|
||||
|
|
|
@ -24,10 +24,11 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import EditorModel from '../../../editor/model';
|
||||
import HistoryManager from '../../../editor/history';
|
||||
import { Caret, setSelection } from '../../../editor/caret';
|
||||
import { formatRange, replaceRangeAndMoveCaret, toggleInlineFormat } from '../../../editor/operations';
|
||||
import { formatRange, formatRangeAsLink, replaceRangeAndMoveCaret, toggleInlineFormat }
|
||||
from '../../../editor/operations';
|
||||
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
|
||||
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
|
||||
import { getAutoCompleteCreator, Type } from '../../../editor/parts';
|
||||
import { getAutoCompleteCreator, Part, Type } from '../../../editor/parts';
|
||||
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
|
||||
import { renderModel } from '../../../editor/render';
|
||||
import TypingStore from "../../../stores/TypingStore";
|
||||
|
@ -45,6 +46,7 @@ import { ICompletion } from "../../../autocomplete/Autocompleter";
|
|||
import { getKeyBindingsManager } from '../../../KeyBindingsManager';
|
||||
import { ALTERNATE_KEY_NAME, KeyBindingAction } from '../../../accessibility/KeyboardShortcuts';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { linkify } from '../../../linkify-matrix';
|
||||
|
||||
// matches emoticons which follow the start of a line or whitespace
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
|
||||
|
@ -90,7 +92,7 @@ function selectionEquals(a: Partial<Selection>, b: Selection): boolean {
|
|||
interface IProps {
|
||||
model: EditorModel;
|
||||
room: Room;
|
||||
threadId: string;
|
||||
threadId?: string;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
initialCaret?: DocumentOffset;
|
||||
|
@ -331,26 +333,32 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => {
|
||||
event.preventDefault(); // we always handle the paste ourselves
|
||||
if (this.props.onPaste && this.props.onPaste(event, this.props.model)) {
|
||||
if (this.props.onPaste?.(event, this.props.model)) {
|
||||
// to prevent double handling, allow props.onPaste to skip internal onPaste
|
||||
return true;
|
||||
}
|
||||
|
||||
const { model } = this.props;
|
||||
const { partCreator } = model;
|
||||
const plainText = event.clipboardData.getData("text/plain");
|
||||
const partsText = event.clipboardData.getData("application/x-element-composer");
|
||||
let parts;
|
||||
|
||||
let parts: Part[];
|
||||
if (partsText) {
|
||||
const serializedTextParts = JSON.parse(partsText);
|
||||
const deserializedParts = serializedTextParts.map(p => partCreator.deserializePart(p));
|
||||
parts = deserializedParts;
|
||||
parts = serializedTextParts.map(p => partCreator.deserializePart(p));
|
||||
} else {
|
||||
const text = event.clipboardData.getData("text/plain");
|
||||
parts = parsePlainTextMessage(text, partCreator, { shouldEscape: false });
|
||||
parts = parsePlainTextMessage(plainText, partCreator, { shouldEscape: false });
|
||||
}
|
||||
|
||||
this.modifiedFlag = true;
|
||||
const range = getRangeForSelection(this.editorRef.current, model, document.getSelection());
|
||||
replaceRangeAndMoveCaret(range, parts);
|
||||
|
||||
if (plainText && range.length > 0 && linkify.test(plainText)) {
|
||||
formatRangeAsLink(range, plainText);
|
||||
} else {
|
||||
replaceRangeAndMoveCaret(range, parts);
|
||||
}
|
||||
};
|
||||
|
||||
private onInput = (event: Partial<InputEvent>): void => {
|
||||
|
|
|
@ -20,27 +20,27 @@ import classNames from 'classnames';
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { MenuItem } from "../../structures/ContextMenu";
|
||||
import { OverflowMenuContext } from './MessageComposerButtons';
|
||||
import { IconizedContextMenuOption } from '../context_menus/IconizedContextMenu';
|
||||
|
||||
interface ICollapsibleButtonProps extends ComponentProps<typeof MenuItem> {
|
||||
title: string;
|
||||
iconClassName: string;
|
||||
}
|
||||
|
||||
export const CollapsibleButton = ({ title, children, className, ...props }: ICollapsibleButtonProps) => {
|
||||
export const CollapsibleButton = ({ title, children, className, iconClassName, ...props }: ICollapsibleButtonProps) => {
|
||||
const inOverflowMenu = !!useContext(OverflowMenuContext);
|
||||
if (inOverflowMenu) {
|
||||
return <MenuItem
|
||||
return <IconizedContextMenuOption
|
||||
{...props}
|
||||
className={classNames("mx_CallContextMenu_item", className)}
|
||||
>
|
||||
{ title }
|
||||
{ children }
|
||||
</MenuItem>;
|
||||
iconClassName={iconClassName}
|
||||
label={title}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
{...props}
|
||||
title={title}
|
||||
className={className}
|
||||
className={classNames(className, iconClassName)}
|
||||
>
|
||||
{ children }
|
||||
</AccessibleTooltipButton>;
|
||||
|
|
|
@ -212,7 +212,7 @@ interface IProps {
|
|||
// whether or not to display thread info
|
||||
showThreadInfo?: boolean;
|
||||
|
||||
// if specified and `true`, the message his behing
|
||||
// if specified and `true`, the message is being
|
||||
// hidden for moderation from other users but is
|
||||
// displayed to the current user either because they're
|
||||
// the author or they are a moderator
|
||||
|
@ -234,7 +234,7 @@ interface IState {
|
|||
// Position of the context menu
|
||||
contextMenu?: {
|
||||
position: Pick<DOMRect, "top" | "left" | "bottom">;
|
||||
showPermalink?: boolean;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
isQuoteExpanded?: boolean;
|
||||
|
@ -842,26 +842,27 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onTimestampContextMenu = (ev: React.MouseEvent): void => {
|
||||
this.showContextMenu(ev, true);
|
||||
this.showContextMenu(ev, this.props.permalinkCreator?.forEvent(this.props.mxEvent.getId()));
|
||||
};
|
||||
|
||||
private showContextMenu(ev: React.MouseEvent, showPermalink?: boolean): void {
|
||||
private showContextMenu(ev: React.MouseEvent, permalink?: string): void {
|
||||
const clickTarget = ev.target as HTMLElement;
|
||||
|
||||
// Return if message right-click context menu isn't enabled
|
||||
if (!SettingsStore.getValue("feature_message_right_click_context_menu")) return;
|
||||
|
||||
// Return if we're in a browser and click either an a tag or we have
|
||||
// selected text, as in those cases we want to use the native browser
|
||||
// menu
|
||||
const clickTarget = ev.target as HTMLElement;
|
||||
if (
|
||||
!PlatformPeg.get().allowOverridingNativeContextMenus() &&
|
||||
(clickTarget.tagName === "a" || clickTarget.closest("a") || getSelectedText())
|
||||
) return;
|
||||
// Try to find an anchor element
|
||||
const anchorElement = (clickTarget instanceof HTMLAnchorElement) ? clickTarget : clickTarget.closest("a");
|
||||
|
||||
// There is no way to copy non-PNG images into clipboard, so we can't
|
||||
// have our own handling for copying images, so we leave it to the
|
||||
// Electron layer (webcontents-handler.ts)
|
||||
if (ev.target instanceof HTMLImageElement) return;
|
||||
if (clickTarget instanceof HTMLImageElement) return;
|
||||
|
||||
// Return if we're in a browser and click either an a tag or we have
|
||||
// selected text, as in those cases we want to use the native browser
|
||||
// menu
|
||||
if (!PlatformPeg.get().allowOverridingNativeContextMenus() && (getSelectedText() || anchorElement)) return;
|
||||
|
||||
// We don't want to show the menu when editing a message
|
||||
if (this.props.editState) return;
|
||||
|
@ -875,7 +876,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
top: ev.clientY,
|
||||
bottom: ev.clientY,
|
||||
},
|
||||
showPermalink: showPermalink,
|
||||
link: anchorElement?.href || permalink,
|
||||
},
|
||||
actionBarFocused: true,
|
||||
});
|
||||
|
@ -924,7 +925,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
onFinished={this.onCloseMenu}
|
||||
rightClick={true}
|
||||
reactions={this.state.reactions}
|
||||
showPermalink={this.state.contextMenu.showPermalink}
|
||||
link={this.state.contextMenu.link}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
|||
import RoomContext from '../../../contexts/RoomContext';
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
||||
import IconizedContextMenu, { IconizedContextMenuOptionList } from '../context_menus/IconizedContextMenu';
|
||||
|
||||
interface IProps {
|
||||
addEmoji: (emoji: string) => boolean;
|
||||
|
@ -108,15 +109,18 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
title={_t("More options")}
|
||||
/> }
|
||||
{ props.isMenuOpen && (
|
||||
<ContextMenu
|
||||
<IconizedContextMenu
|
||||
onFinished={props.toggleButtonMenu}
|
||||
{...props.menuPosition}
|
||||
wrapperClassName="mx_MessageComposer_Menu"
|
||||
compact={true}
|
||||
>
|
||||
<OverflowMenuContext.Provider value={props.toggleButtonMenu}>
|
||||
{ moreButtons }
|
||||
<IconizedContextMenuOptionList>
|
||||
{ moreButtons }
|
||||
</IconizedContextMenuOptionList>
|
||||
</OverflowMenuContext.Provider>
|
||||
</ContextMenu>
|
||||
</IconizedContextMenu>
|
||||
) }
|
||||
</UploadButtonContextProvider>;
|
||||
};
|
||||
|
@ -158,7 +162,6 @@ const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) =>
|
|||
|
||||
const className = classNames(
|
||||
"mx_MessageComposer_button",
|
||||
"mx_MessageComposer_emoji",
|
||||
{
|
||||
"mx_MessageComposer_button_highlight": menuDisplayed,
|
||||
},
|
||||
|
@ -169,6 +172,7 @@ const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) =>
|
|||
return <React.Fragment>
|
||||
<CollapsibleButton
|
||||
className={className}
|
||||
iconClassName="mx_MessageComposer_emoji"
|
||||
onClick={openMenu}
|
||||
title={_t("Emoji")}
|
||||
/>
|
||||
|
@ -254,7 +258,8 @@ const UploadButton = () => {
|
|||
};
|
||||
|
||||
return <CollapsibleButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_upload"
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_upload"
|
||||
onClick={onClick}
|
||||
title={_t('Attachment')}
|
||||
/>;
|
||||
|
@ -266,7 +271,8 @@ function showStickersButton(props: IProps): ReactElement {
|
|||
? <CollapsibleButton
|
||||
id='stickersButton'
|
||||
key="controls_stickers"
|
||||
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_stickers"
|
||||
onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)}
|
||||
title={props.isStickerPickerOpen ? _t("Hide stickers") : _t("Sticker")}
|
||||
/>
|
||||
|
@ -281,7 +287,8 @@ function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement {
|
|||
? null
|
||||
: <CollapsibleButton
|
||||
key="voice_message_send"
|
||||
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_voiceMessage"
|
||||
onClick={props.onRecordStartEndClick}
|
||||
title={_t("Voice Message")}
|
||||
/>
|
||||
|
@ -345,7 +352,8 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
|
|||
|
||||
return (
|
||||
<CollapsibleButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_poll"
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_poll"
|
||||
onClick={this.onCreateClick}
|
||||
title={_t("Poll")}
|
||||
/>
|
||||
|
|
|
@ -66,7 +66,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
|
|||
request.on(VerificationRequestEvent.Change, this.checkRequestIsPending);
|
||||
// We should probably have a separate class managing the active verification toasts,
|
||||
// rather than monitoring this in the toast component itself, since we'll get problems
|
||||
// like the toasdt not going away when the verification is cancelled unless it's the
|
||||
// like the toast not going away when the verification is cancelled unless it's the
|
||||
// one on the top (ie. the one that's mounted).
|
||||
// As a quick & dirty fix, check the toast is still relevant when it mounts (this prevents
|
||||
// a toast hanging around after logging in if you did a verification as part of login).
|
||||
|
|
|
@ -28,7 +28,7 @@ export enum ComposerType {
|
|||
interface IBaseComposerInsertPayload extends ActionPayload {
|
||||
action: Action.ComposerInsert;
|
||||
timelineRenderingType: TimelineRenderingType;
|
||||
composerType?: ComposerType; // falsey if should be re-dispatched to the correct composer
|
||||
composerType?: ComposerType; // falsy if should be re-dispatched to the correct composer
|
||||
}
|
||||
|
||||
interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {
|
||||
|
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||
|
||||
import { CARET_NODE_CHAR, isCaretNode } from "./render";
|
||||
import DocumentOffset from "./offset";
|
||||
import EditorModel from "./model";
|
||||
import Range from "./range";
|
||||
|
||||
type Predicate = (node: Node) => boolean;
|
||||
type Callback = (node: Node) => void;
|
||||
|
@ -122,7 +124,7 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) {
|
|||
let foundNode = false;
|
||||
let text = "";
|
||||
|
||||
function enterNodeCallback(node) {
|
||||
function enterNodeCallback(node: HTMLElement) {
|
||||
if (!foundNode) {
|
||||
if (node === selectionNode) {
|
||||
foundNode = true;
|
||||
|
@ -148,12 +150,12 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function leaveNodeCallback(node) {
|
||||
function leaveNodeCallback(node: HTMLElement) {
|
||||
// if this is not the last DIV (which are only used as line containers atm)
|
||||
// we don't just check if there is a nextSibling because sometimes the caret ends up
|
||||
// after the last DIV and it creates a newline if you type then,
|
||||
// whereas you just want it to be appended to the current line
|
||||
if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") {
|
||||
if (node.tagName === "DIV" && (<HTMLElement>node.nextSibling)?.tagName === "DIV") {
|
||||
text += "\n";
|
||||
if (!foundNode) {
|
||||
offsetToNode += 1;
|
||||
|
@ -167,7 +169,7 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) {
|
|||
}
|
||||
|
||||
// get text value of text node, ignoring ZWS if it's a caret node
|
||||
function getTextNodeValue(node) {
|
||||
function getTextNodeValue(node: Node): string {
|
||||
const nodeText = node.nodeValue;
|
||||
// filter out ZWS for caret nodes
|
||||
if (isCaretNode(node.parentElement)) {
|
||||
|
@ -176,7 +178,7 @@ function getTextNodeValue(node) {
|
|||
if (nodeText.length !== 1) {
|
||||
return nodeText.replace(CARET_NODE_CHAR, "");
|
||||
} else {
|
||||
// only contains ZWS, which is ignored, so return emtpy string
|
||||
// only contains ZWS, which is ignored, so return empty string
|
||||
return "";
|
||||
}
|
||||
} else {
|
||||
|
@ -184,7 +186,7 @@ function getTextNodeValue(node) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getRangeForSelection(editor, model, selection) {
|
||||
export function getRangeForSelection(editor: HTMLDivElement, model: EditorModel, selection: Selection): Range {
|
||||
const focusOffset = getSelectionOffsetAndText(
|
||||
editor,
|
||||
selection.focusNode,
|
||||
|
|
|
@ -37,7 +37,7 @@ export function formatRange(range: Range, action: Formatting): void {
|
|||
range.trim();
|
||||
}
|
||||
|
||||
// Edgecase when just selecting whitespace or new line.
|
||||
// Edge case when just selecting whitespace or new line.
|
||||
// There should be no reason to format whitespace, so we can just return.
|
||||
if (range.length === 0) {
|
||||
return;
|
||||
|
@ -216,20 +216,18 @@ export function formatRangeAsCode(range: Range): void {
|
|||
replaceRangeAndExpandSelection(range, parts);
|
||||
}
|
||||
|
||||
export function formatRangeAsLink(range: Range) {
|
||||
export function formatRangeAsLink(range: Range, text?: string) {
|
||||
const { model } = range;
|
||||
const { partCreator } = model;
|
||||
const linkRegex = /\[(.*?)\]\(.*?\)/g;
|
||||
const linkRegex = /\[(.*?)]\(.*?\)/g;
|
||||
const isFormattedAsLink = linkRegex.test(range.text);
|
||||
if (isFormattedAsLink) {
|
||||
const linkDescription = range.text.replace(linkRegex, "$1");
|
||||
const newParts = [partCreator.plain(linkDescription)];
|
||||
const prefixLength = 1;
|
||||
const suffixLength = range.length - (linkDescription.length + 2);
|
||||
replaceRangeAndAutoAdjustCaret(range, newParts, true, prefixLength, suffixLength);
|
||||
replaceRangeAndMoveCaret(range, newParts, 0);
|
||||
} else {
|
||||
// We set offset to -1 here so that the caret lands between the brackets
|
||||
replaceRangeAndMoveCaret(range, [partCreator.plain("[" + range.text + "]" + "()")], -1);
|
||||
replaceRangeAndMoveCaret(range, [partCreator.plain("[" + range.text + "]" + "(" + (text ?? "") + ")")], -1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2921,10 +2921,10 @@
|
|||
"Forward": "Forward",
|
||||
"View source": "View source",
|
||||
"Show preview": "Show preview",
|
||||
"Copy link": "Copy link",
|
||||
"Source URL": "Source URL",
|
||||
"Collapse reply thread": "Collapse reply thread",
|
||||
"Report": "Report",
|
||||
"Copy link": "Copy link",
|
||||
"Forget": "Forget",
|
||||
"Mentions only": "Mentions only",
|
||||
"See room timeline (devtools)": "See room timeline (devtools)",
|
||||
|
@ -2957,11 +2957,11 @@
|
|||
"View List": "View List",
|
||||
"Close sidebar": "Close sidebar",
|
||||
"An error occurred while stopping your live location": "An error occurred while stopping your live location",
|
||||
"An error occured whilst sharing your live location": "An error occured whilst sharing your live location",
|
||||
"An error occurred whilst sharing your live location": "An error occurred whilst sharing your live location",
|
||||
"You are sharing your live location": "You are sharing your live location",
|
||||
"%(timeRemaining)s left": "%(timeRemaining)s left",
|
||||
"Live location enabled": "Live location enabled",
|
||||
"An error occured whilst sharing your live location, please try again": "An error occured whilst sharing your live location, please try again",
|
||||
"An error occurred whilst sharing your live location, please try again": "An error occurred whilst sharing your live location, please try again",
|
||||
"An error occurred while stopping your live location, please try again": "An error occurred while stopping your live location, please try again",
|
||||
"Stop sharing": "Stop sharing",
|
||||
"Stop sharing and close": "Stop sharing and close",
|
||||
|
|
|
@ -91,7 +91,7 @@ export default abstract class BaseEventIndexManager {
|
|||
*
|
||||
* @param {MatrixEvent} ev The event that should be added to the index.
|
||||
* @param {IMatrixProfile} profile The profile of the event sender at the
|
||||
* time of the event receival.
|
||||
* time the event was received.
|
||||
*
|
||||
* @return {Promise} A promise that will resolve when the was queued up for
|
||||
* addition.
|
||||
|
|
|
@ -596,7 +596,7 @@ export default class EventIndex extends EventEmitter {
|
|||
continue;
|
||||
}
|
||||
|
||||
// If all events were already indexed we assume that we catched
|
||||
// If all events were already indexed we assume that we caught
|
||||
// up with our index and don't need to crawl the room further.
|
||||
// Let us delete the checkpoint in that case, otherwise push
|
||||
// the new checkpoint to be used by the crawler.
|
||||
|
@ -612,7 +612,7 @@ export default class EventIndex extends EventEmitter {
|
|||
this.crawlerCheckpoints.push(newCheckpoint);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.log("EventIndex: Error durring a crawl", e);
|
||||
logger.log("EventIndex: Error during a crawl", e);
|
||||
// An error occurred, put the checkpoint back so we
|
||||
// can retry.
|
||||
this.crawlerCheckpoints.push(checkpoint);
|
||||
|
@ -797,7 +797,7 @@ export default class EventIndex extends EventEmitter {
|
|||
// to get our events in the BACKWARDS direction but populate them in the
|
||||
// forwards direction.
|
||||
// This needs to happen because a fill request might come with an
|
||||
// exisitng timeline e.g. if you close and re-open the FilePanel.
|
||||
// existing timeline e.g. if you close and re-open the FilePanel.
|
||||
if (fromEvent === null) {
|
||||
matrixEvents.reverse();
|
||||
direction = direction == EventTimeline.BACKWARDS ? EventTimeline.FORWARDS: EventTimeline.BACKWARDS;
|
||||
|
|
|
@ -67,7 +67,7 @@ export class EventIndexPeg {
|
|||
/**
|
||||
* Initialize the event index.
|
||||
*
|
||||
* @returns {boolean} True if the event index was succesfully initialized,
|
||||
* @returns {boolean} True if the event index was successfully initialized,
|
||||
* false otherwise.
|
||||
*/
|
||||
async initEventIndex() {
|
||||
|
@ -118,7 +118,7 @@ export class EventIndexPeg {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if event indexing support is installed for the platfrom.
|
||||
* Check if event indexing support is installed for the platform.
|
||||
*
|
||||
* Event indexing might require additional optional modules to be installed,
|
||||
* this tells us if those are installed. Note that this should only be
|
||||
|
|
|
@ -75,7 +75,7 @@ export class IntegrationManagerInstance {
|
|||
client.setTermsInteractionCallback((policyInfo, agreedUrls) => {
|
||||
// To avoid visual glitching of two modals stacking briefly, we customise the
|
||||
// terms dialog sizing when it will appear for the integration manager so that
|
||||
// it gets the same basic size as the IM's own modal.
|
||||
// it gets the same basic size as the integration manager's own modal.
|
||||
return dialogTermsInteractionCallback(
|
||||
policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager',
|
||||
);
|
||||
|
|
|
@ -132,7 +132,7 @@ export const VectorPushRulesDefinitions = {
|
|||
}),
|
||||
|
||||
// Messages just sent to a group chat room
|
||||
// 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined
|
||||
// 1:1 room messages are caught by the .m.rule.room_one_to_one rule if any defined
|
||||
// By opposition, all other room messages are from group chat rooms.
|
||||
".m.rule.message": new VectorPushRuleDefinition({
|
||||
description: _td("Messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||
|
@ -144,7 +144,7 @@ export const VectorPushRulesDefinitions = {
|
|||
}),
|
||||
|
||||
// Encrypted messages just sent to a group chat room
|
||||
// Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined
|
||||
// Encrypted 1:1 room messages are caught by the .m.rule.encrypted_room_one_to_one rule if any defined
|
||||
// By opposition, all other room messages are from group chat rooms.
|
||||
".m.rule.encrypted": new VectorPushRuleDefinition({
|
||||
description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||
|
|
|
@ -100,7 +100,7 @@ export default class ThemeWatcher {
|
|||
// itself completely redundant since we just override the result here and we're
|
||||
// now effectively just using the ThemeController as a place to store the static
|
||||
// variable. The system theme setting probably ought to have an equivalent
|
||||
// controller that honours the same flag, although probablt better would be to
|
||||
// controller that honours the same flag, although probably better would be to
|
||||
// have the theme logic in one place rather than split between however many
|
||||
// different places.
|
||||
if (ThemeController.isLogin) return 'light';
|
||||
|
|
|
@ -43,7 +43,7 @@ import {
|
|||
TimedGeoUri,
|
||||
watchPosition,
|
||||
} from "../utils/beacon";
|
||||
import { getCurrentPosition } from "../utils/beacon/geolocation";
|
||||
import { getCurrentPosition } from "../utils/beacon";
|
||||
|
||||
const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconInfoOwner === userId;
|
||||
|
||||
|
@ -456,7 +456,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
private onWatchedPosition = (position: GeolocationPosition) => {
|
||||
const timedGeoPosition = mapGeolocationPositionToTimedGeo(position);
|
||||
|
||||
// if this is our first position, publish immediateley
|
||||
// if this is our first position, publish immediately
|
||||
if (!this.lastPublishedPositionTimestamp) {
|
||||
this.publishLocationToBeacons(timedGeoPosition);
|
||||
} else {
|
||||
|
|
|
@ -504,7 +504,7 @@ export class RoomViewStore extends Store<ActionPayload> {
|
|||
// since we should still consider a join to be in progress until the room
|
||||
// & member events come down the sync.
|
||||
//
|
||||
// This flag remains true after the room has been sucessfully joined,
|
||||
// This flag remains true after the room has been successfully joined,
|
||||
// (this store doesn't listen for the appropriate member events)
|
||||
// so you should always observe the joined state from the member event
|
||||
// if a room object is present.
|
||||
|
|
|
@ -159,6 +159,10 @@ export default class VideoChannelStore extends AsyncStoreWithClient<null> {
|
|||
messaging.on(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
messaging.on(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
messaging.on(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
// Empirically, it's possible for Jitsi Meet to crash instantly at startup,
|
||||
// sending a hangup event that races with the rest of this method, so we also
|
||||
// need to add the hangup listener now rather than later
|
||||
messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
this.emit(VideoChannelEvent.StartConnect, roomId);
|
||||
|
||||
|
@ -186,6 +190,7 @@ export default class VideoChannelStore extends AsyncStoreWithClient<null> {
|
|||
messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
messaging.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
messaging.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
messaging.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
this.emit(VideoChannelEvent.Disconnect, roomId);
|
||||
|
||||
|
@ -193,7 +198,6 @@ export default class VideoChannelStore extends AsyncStoreWithClient<null> {
|
|||
}
|
||||
|
||||
this.connected = true;
|
||||
messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.matrixClient.getRoom(roomId).on(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.addEventListener("beforeunload", this.setDisconnected);
|
||||
|
||||
|
@ -258,6 +262,9 @@ export default class VideoChannelStore extends AsyncStoreWithClient<null> {
|
|||
|
||||
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this.ack(ev);
|
||||
// In case this hangup is caused by Jitsi Meet crashing at startup,
|
||||
// wait for the connection event in order to avoid racing
|
||||
if (!this.connected) await waitForEvent(this, VideoChannelEvent.Connect);
|
||||
await this.setDisconnected();
|
||||
};
|
||||
|
||||
|
|
|
@ -44,9 +44,9 @@ class WidgetEchoStore extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the widgets for a room, substracting those that are pending deletion.
|
||||
* Gets the widgets for a room, subtracting those that are pending deletion.
|
||||
* Widgets that are pending addition are not included, since widgets are
|
||||
* represted as MatrixEvents, so to do this we'd have to create fake MatrixEvents,
|
||||
* represented as MatrixEvents, so to do this we'd have to create fake MatrixEvents,
|
||||
* and we don't really need the actual widget events anyway since we just want to
|
||||
* show a spinner / prevent widgets being added twice.
|
||||
*
|
||||
|
|
|
@ -38,7 +38,7 @@ const traverseSpaceDescendants = (
|
|||
};
|
||||
|
||||
/**
|
||||
* Helper function to traverse space heirachy and flatten
|
||||
* Helper function to traverse space hierarchy and flatten
|
||||
* @param spaceEntityMap ie map of rooms or dm userIds
|
||||
* @param spaceDescendantMap map of spaces and their children
|
||||
* @returns set of all rooms
|
||||
|
|
|
@ -91,7 +91,7 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
|||
/**
|
||||
* Gets the widget messaging class for a given widget UID.
|
||||
* @param {string} widgetUid The widget UID.
|
||||
* @returns {ClientWidgetApi} The widget API, or a falsey value if not found.
|
||||
* @returns {ClientWidgetApi} The widget API, or a falsy value if not found.
|
||||
*/
|
||||
public getMessagingForUid(widgetUid: string): ClientWidgetApi {
|
||||
return this.widgetMap.get(widgetUid);
|
||||
|
|
|
@ -142,7 +142,7 @@ export default class DMRoomMap {
|
|||
/**
|
||||
* Gets the DM room which the given IDs share, if any.
|
||||
* @param {string[]} ids The identifiers (user IDs and email addresses) to look for.
|
||||
* @returns {Room} The DM room which all IDs given share, or falsey if no common room.
|
||||
* @returns {Room} The DM room which all IDs given share, or falsy if no common room.
|
||||
*/
|
||||
public getDMRoomForIdentifiers(ids: string[]): Room {
|
||||
// TODO: [Canonical DMs] Handle lookups for email addresses.
|
||||
|
|
|
@ -25,7 +25,7 @@ import { _t, _td, Tags, TranslatedString } from '../languageHandler';
|
|||
*
|
||||
* @param {string} limitType The limit_type from the error
|
||||
* @param {string} adminContact The admin_contact from the error
|
||||
* @param {Object} strings Translateable string for different
|
||||
* @param {Object} strings Translatable string for different
|
||||
* limit_type. Must include at least the empty string key
|
||||
* which is the default. Strings may include an 'a' tag
|
||||
* for the admin contact link.
|
||||
|
|
|
@ -64,7 +64,7 @@ export function presentableTextForFile(
|
|||
// big a file they are downloading.
|
||||
// The content.info also contains a MIME-type but we don't display
|
||||
// it since it is "ugly", users generally aren't aware what it
|
||||
// means and the type of the attachment can usually be inferrered
|
||||
// means and the type of the attachment can usually be inferred
|
||||
// from the file extension.
|
||||
text += ' (' + filesize(content.info.size) + ')';
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export class LazyValue<T> {
|
|||
* Whether or not a cached value is present.
|
||||
*/
|
||||
public get present(): boolean {
|
||||
// we use a tracking variable just in case the final value is falsey
|
||||
// we use a tracking variable just in case the final value is falsy
|
||||
return this.done;
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ const keyMap = new EnhancedMap<Object, EnhancedMap<string, unknown>>();
|
|||
* second call comes through late. There are various functions named "forget"
|
||||
* to have the cache be cleared of a result.
|
||||
*
|
||||
* Singleflights in our usecase are tied to an instance of something, combined
|
||||
* Singleflights in our use case are tied to an instance of something, combined
|
||||
* with a string key to differentiate between multiple possible actions. This
|
||||
* means that a "save" key will be scoped to the instance which defined it and
|
||||
* not leak between other instances. This is done to avoid having to concatenate
|
||||
|
|
|
@ -54,7 +54,7 @@ export const useBeacon = (beaconInfoEvent: MatrixEvent): Beacon | undefined => {
|
|||
}
|
||||
}, [beaconInfoEvent, matrixClient]);
|
||||
|
||||
// beacon update will fire when this beacon is superceded
|
||||
// beacon update will fire when this beacon is superseded
|
||||
// check the updated event id for equality to the matrix event
|
||||
const beaconInstanceEventId = useEventEmitterState(
|
||||
beacon,
|
||||
|
|
|
@ -175,7 +175,7 @@ export class ThreepidMember extends Member {
|
|||
this.id = id;
|
||||
}
|
||||
|
||||
// This is a getter that would be falsey on all other implementations. Until we have
|
||||
// This is a getter that would be falsy on all other implementations. Until we have
|
||||
// better type support in the react-sdk we can use this trick to determine the kind
|
||||
// of 3PID we're dealing with, if any.
|
||||
get isEmail(): boolean {
|
||||
|
|
|
@ -120,7 +120,7 @@ a.mx_reply_anchor:hover {
|
|||
}
|
||||
|
||||
.mx_ReplyChain_Export {
|
||||
margin-top: 0px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
|
|||
}
|
||||
|
||||
/**
|
||||
* Parses an app route (`(user|room)/identifer`) to a Matrix entity
|
||||
* Parses an app route (`(user|room)/identifier`) to a Matrix entity
|
||||
* (room, user).
|
||||
* @param {string} route The app route
|
||||
* @returns {PermalinkParts}
|
||||
|
|
|
@ -274,7 +274,7 @@ export function makeUserPermalink(userId: string): string {
|
|||
|
||||
export function makeRoomPermalink(roomId: string): string {
|
||||
if (!roomId) {
|
||||
throw new Error("can't permalink a falsey roomId");
|
||||
throw new Error("can't permalink a falsy roomId");
|
||||
}
|
||||
|
||||
// If the roomId isn't actually a room ID, don't try to list the servers.
|
||||
|
|
|
@ -168,7 +168,7 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
const component = getComponent();
|
||||
// error mode
|
||||
expect(component.find('.mx_LeftPanelLiveShareWarning').at(0).text()).toEqual(
|
||||
'An error occured whilst sharing your live location',
|
||||
'An error occurred whilst sharing your live location',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
|
|
|
@ -359,7 +359,7 @@ describe('<RoomLiveShareWarning />', () => {
|
|||
|
||||
// renders wire error ui
|
||||
expect(component.find('.mx_RoomLiveShareWarning_label').text()).toEqual(
|
||||
'An error occured whilst sharing your live location, please try again',
|
||||
'An error occurred whilst sharing your live location, please try again',
|
||||
);
|
||||
expect(findByTestId(component, 'room-live-share-wire-error-close-button').length).toBeTruthy();
|
||||
});
|
||||
|
|
|
@ -69,7 +69,7 @@ exports[`<LeftPanelLiveShareWarning /> when user has live location monitor rende
|
|||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
An error occured whilst sharing your live location
|
||||
An error occurred whilst sharing your live location
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</LeftPanelLiveShareWarning>
|
||||
|
|
|
@ -32,7 +32,7 @@ exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is
|
|||
<span
|
||||
className="mx_RoomLiveShareWarning_label"
|
||||
>
|
||||
An error occured whilst sharing your live location, please try again
|
||||
An error occurred whilst sharing your live location, please try again
|
||||
</span>
|
||||
<AccessibleButton
|
||||
className="mx_RoomLiveShareWarning_stopButton"
|
||||
|
|
65
test/components/views/rooms/BasicMessageComposer-test.tsx
Normal file
65
test/components/views/rooms/BasicMessageComposer-test.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2022 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 { mount, ReactWrapper } from 'enzyme';
|
||||
import { MatrixClient, Room } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
import BasicMessageComposer from '../../../../src/components/views/rooms/BasicMessageComposer';
|
||||
import * as TestUtils from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import EditorModel from "../../../../src/editor/model";
|
||||
import { createPartCreator, createRenderer } from "../../../editor/mock";
|
||||
|
||||
describe("BasicMessageComposer", () => {
|
||||
const renderer = createRenderer();
|
||||
const pc = createPartCreator();
|
||||
|
||||
beforeEach(() => {
|
||||
TestUtils.stubClient();
|
||||
});
|
||||
|
||||
it("should allow a user to paste a URL without it being mangled", () => {
|
||||
const model = new EditorModel([], pc, renderer);
|
||||
|
||||
const wrapper = render(model);
|
||||
|
||||
wrapper.find(".mx_BasicMessageComposer_input").simulate("paste", {
|
||||
clipboardData: {
|
||||
getData: type => {
|
||||
if (type === "text/plain") {
|
||||
return "https://element.io";
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(model.parts).toHaveLength(1);
|
||||
expect(model.parts[0].text).toBe("https://element.io");
|
||||
});
|
||||
});
|
||||
|
||||
function render(model: EditorModel): ReactWrapper {
|
||||
const client: MatrixClient = MatrixClientPeg.get();
|
||||
|
||||
const roomId = '!1234567890:domain';
|
||||
const userId = client.getUserId();
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
return mount((
|
||||
<BasicMessageComposer model={model} room={room} />
|
||||
));
|
||||
}
|
|
@ -17,7 +17,11 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import ReactTestUtils from 'react-dom/test-utils';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk/src/matrix';
|
||||
import {
|
||||
PendingEventOrdering,
|
||||
Room,
|
||||
RoomMember,
|
||||
} from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
import * as TestUtils from '../../../test-utils';
|
||||
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||
|
@ -29,6 +33,8 @@ import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayout
|
|||
import RoomList from "../../../../src/components/views/rooms/RoomList";
|
||||
import RoomSublist from "../../../../src/components/views/rooms/RoomSublist";
|
||||
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from '../../../test-utils';
|
||||
import ResizeNotifier from '../../../../src/utils/ResizeNotifier';
|
||||
|
||||
function generateRoomId() {
|
||||
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
||||
|
@ -38,7 +44,7 @@ describe('RoomList', () => {
|
|||
function createRoom(opts) {
|
||||
const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
|
||||
// The room list now uses getPendingEvents(), so we need a detached ordering.
|
||||
pendingEventOrdering: "detached",
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
if (opts) {
|
||||
Object.assign(room, opts);
|
||||
|
@ -47,25 +53,38 @@ describe('RoomList', () => {
|
|||
}
|
||||
|
||||
let parentDiv = null;
|
||||
let client = null;
|
||||
let root = null;
|
||||
const myUserId = '@me:domain';
|
||||
|
||||
const movingRoomId = '!someroomid';
|
||||
let movingRoom;
|
||||
let otherRoom;
|
||||
let movingRoom: Room | undefined;
|
||||
let otherRoom: Room | undefined;
|
||||
|
||||
let myMember;
|
||||
let myOtherMember;
|
||||
let myMember: RoomMember | undefined;
|
||||
let myOtherMember: RoomMember | undefined;
|
||||
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(myUserId),
|
||||
getRooms: jest.fn(),
|
||||
getVisibleRooms: jest.fn(),
|
||||
getRoom: jest.fn(),
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
onKeyDown: jest.fn(),
|
||||
onFocus: jest.fn(),
|
||||
onBlur: jest.fn(),
|
||||
onResize: jest.fn(),
|
||||
resizeNotifier: {} as unknown as ResizeNotifier,
|
||||
isMinimized: false,
|
||||
activeSpace: '',
|
||||
};
|
||||
|
||||
beforeEach(async function(done) {
|
||||
RoomListStoreClass.TEST_MODE = true;
|
||||
jest.clearAllMocks();
|
||||
|
||||
TestUtils.stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
client.credentials = { userId: myUserId };
|
||||
//revert this to prototype method as the test-utils monkey-patches this to return a hardcoded value
|
||||
client.getUserId = MatrixClient.prototype.getUserId;
|
||||
|
||||
DMRoomMap.makeShared();
|
||||
|
||||
|
@ -74,7 +93,7 @@ describe('RoomList', () => {
|
|||
|
||||
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
|
||||
root = ReactDOM.render(
|
||||
<WrappedRoomList searchFilter="" onResize={() => {}} />,
|
||||
<WrappedRoomList {...defaultProps} />,
|
||||
parentDiv,
|
||||
);
|
||||
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
|
||||
|
@ -99,7 +118,7 @@ describe('RoomList', () => {
|
|||
}[userId]);
|
||||
|
||||
// Mock the matrix client
|
||||
client.getRooms = () => [
|
||||
const mockRooms = [
|
||||
movingRoom,
|
||||
otherRoom,
|
||||
createRoom({ tags: { 'm.favourite': { order: 0.1 } }, name: 'Some other room' }),
|
||||
|
@ -107,14 +126,15 @@ describe('RoomList', () => {
|
|||
createRoom({ tags: { 'm.lowpriority': {} }, name: 'Some unimportant room' }),
|
||||
createRoom({ tags: { 'custom.tag': {} }, name: 'Some room customly tagged' }),
|
||||
];
|
||||
client.getVisibleRooms = client.getRooms;
|
||||
client.getRooms.mockReturnValue(mockRooms);
|
||||
client.getVisibleRooms.mockReturnValue(mockRooms);
|
||||
|
||||
const roomMap = {};
|
||||
client.getRooms().forEach((r) => {
|
||||
roomMap[r.roomId] = r;
|
||||
});
|
||||
|
||||
client.getRoom = (roomId) => roomMap[roomId];
|
||||
client.getRoom.mockImplementation((roomId) => roomMap[roomId]);
|
||||
|
||||
// Now that everything has been set up, prepare and update the store
|
||||
await RoomListStore.instance.makeReady(client);
|
||||
|
@ -171,6 +191,7 @@ describe('RoomList', () => {
|
|||
movingRoom.tags = { [oldTagId]: {} };
|
||||
} else if (oldTagId === DefaultTagID.DM) {
|
||||
// Mock inverse m.direct
|
||||
// @ts-ignore forcing private property
|
||||
DMRoomMap.shared().roomToUser = {
|
||||
[movingRoom.roomId]: '@someotheruser:domain',
|
||||
};
|
|
@ -18,6 +18,7 @@ import { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
|
|||
|
||||
import AutocompleteWrapperModel from "../../src/editor/autocomplete";
|
||||
import { PartCreator } from "../../src/editor/parts";
|
||||
import DocumentPosition from "../../src/editor/position";
|
||||
|
||||
class MockAutoComplete {
|
||||
public _updateCallback;
|
||||
|
@ -78,11 +79,11 @@ export function createPartCreator(completions = []) {
|
|||
}
|
||||
|
||||
export function createRenderer() {
|
||||
const render = (c) => {
|
||||
const render = (c: DocumentPosition) => {
|
||||
render.caret = c;
|
||||
render.count += 1;
|
||||
};
|
||||
render.count = 0;
|
||||
render.caret = null;
|
||||
render.caret = null as DocumentPosition;
|
||||
return render;
|
||||
}
|
||||
|
|
|
@ -17,21 +17,88 @@ limitations under the License.
|
|||
import EditorModel from "../../src/editor/model";
|
||||
import { createPartCreator, createRenderer } from "./mock";
|
||||
import {
|
||||
toggleInlineFormat,
|
||||
selectRangeOfWordAtCaret,
|
||||
formatRange,
|
||||
formatRangeAsCode,
|
||||
formatRangeAsLink,
|
||||
selectRangeOfWordAtCaret,
|
||||
toggleInlineFormat,
|
||||
} from "../../src/editor/operations";
|
||||
import { Formatting } from "../../src/components/views/rooms/MessageComposerFormatBar";
|
||||
import { longestBacktickSequence } from '../../src/editor/deserialize';
|
||||
|
||||
const SERIALIZED_NEWLINE = { "text": "\n", "type": "newline" };
|
||||
|
||||
describe('editor/operations: formatting operations', () => {
|
||||
describe('toggleInlineFormat', () => {
|
||||
it('works for words', () => {
|
||||
const renderer = createRenderer();
|
||||
const pc = createPartCreator();
|
||||
describe("editor/operations: formatting operations", () => {
|
||||
const renderer = createRenderer();
|
||||
const pc = createPartCreator();
|
||||
|
||||
describe("formatRange", () => {
|
||||
it.each([
|
||||
[Formatting.Bold, "hello **world**!"],
|
||||
])("should correctly wrap format %s", (formatting: Formatting, expected: string) => {
|
||||
const model = new EditorModel([
|
||||
pc.plain("hello world!"),
|
||||
], pc, renderer);
|
||||
|
||||
const range = model.startRange(model.positionForOffset(6, false),
|
||||
model.positionForOffset(11, false)); // around "world"
|
||||
|
||||
expect(range.parts[0].text).toBe("world");
|
||||
expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]);
|
||||
formatRange(range, formatting);
|
||||
expect(model.serializeParts()).toEqual([{ "text": expected, "type": "plain" }]);
|
||||
});
|
||||
|
||||
it("should apply to word range is within if length 0", () => {
|
||||
const model = new EditorModel([
|
||||
pc.plain("hello world!"),
|
||||
], pc, renderer);
|
||||
|
||||
const range = model.startRange(model.positionForOffset(6, false));
|
||||
|
||||
expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]);
|
||||
formatRange(range, Formatting.Bold);
|
||||
expect(model.serializeParts()).toEqual([{ "text": "hello **world!**", "type": "plain" }]);
|
||||
});
|
||||
|
||||
it("should do nothing for a range with length 0 at initialisation", () => {
|
||||
const model = new EditorModel([
|
||||
pc.plain("hello world!"),
|
||||
], pc, renderer);
|
||||
|
||||
const range = model.startRange(model.positionForOffset(6, false));
|
||||
range.setWasEmpty(false);
|
||||
|
||||
expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]);
|
||||
formatRange(range, Formatting.Bold);
|
||||
expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatRangeAsLink", () => {
|
||||
it.each([
|
||||
// Caret is denoted by | in the expectation string
|
||||
["testing", "[testing](|)", ""],
|
||||
["testing", "[testing](foobar|)", "foobar"],
|
||||
["[testing]()", "testing|", ""],
|
||||
["[testing](foobar)", "testing|", ""],
|
||||
])("converts %s -> %s", (input: string, expectation: string, text: string) => {
|
||||
const model = new EditorModel([
|
||||
pc.plain(`foo ${input} bar`),
|
||||
], pc, renderer);
|
||||
|
||||
const range = model.startRange(model.positionForOffset(4, false),
|
||||
model.positionForOffset(4 + input.length, false)); // around input
|
||||
|
||||
expect(range.parts[0].text).toBe(input);
|
||||
formatRangeAsLink(range, text);
|
||||
expect(renderer.caret.offset).toBe(4 + expectation.indexOf("|"));
|
||||
expect(model.parts[0].text).toBe("foo " + expectation.replace("|", "") + " bar");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleInlineFormat", () => {
|
||||
it("works for words", () => {
|
||||
const model = new EditorModel([
|
||||
pc.plain("hello world!"),
|
||||
], pc, renderer);
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 - 2022 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 { RoomViewStore } from '../../src/stores/RoomViewStore';
|
||||
import { Action } from '../../src/dispatcher/actions';
|
||||
import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg';
|
||||
import * as testUtils from '../test-utils';
|
||||
|
||||
const dispatch = testUtils.getDispatchForStore(RoomViewStore.instance);
|
||||
|
||||
jest.mock('../../src/utils/DMRoomMap', () => {
|
||||
const mock = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
getDMRoomsForUserId: jest.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
shared: jest.fn().mockReturnValue(mock),
|
||||
sharedInstance: mock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('RoomViewStore', function() {
|
||||
beforeEach(function() {
|
||||
testUtils.stubClient();
|
||||
peg.get().credentials = { userId: "@test:example.com" };
|
||||
peg.get().on = jest.fn();
|
||||
peg.get().off = jest.fn();
|
||||
|
||||
// Reset the state of the store
|
||||
RoomViewStore.instance.reset();
|
||||
});
|
||||
|
||||
it('can be used to view a room by ID and join', function(done) {
|
||||
peg.get().joinRoom = async (roomAddress) => {
|
||||
expect(roomAddress).toBe("!randomcharacters:aser.ver");
|
||||
done();
|
||||
};
|
||||
|
||||
dispatch({ action: Action.ViewRoom, room_id: '!randomcharacters:aser.ver' });
|
||||
dispatch({ action: 'join_room' });
|
||||
expect(RoomViewStore.instance.isJoining()).toBe(true);
|
||||
});
|
||||
|
||||
it('can be used to view a room by alias and join', function(done) {
|
||||
const token = RoomViewStore.instance.addListener(() => {
|
||||
// Wait until the room alias has resolved and the room ID is
|
||||
if (!RoomViewStore.instance.isRoomLoading()) {
|
||||
expect(RoomViewStore.instance.getRoomId()).toBe("!randomcharacters:aser.ver");
|
||||
dispatch({ action: 'join_room' });
|
||||
expect(RoomViewStore.instance.isJoining()).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
peg.get().getRoomIdForAlias.mockResolvedValue({ room_id: "!randomcharacters:aser.ver" });
|
||||
peg.get().joinRoom = async (roomAddress) => {
|
||||
token.remove(); // stop RVS listener
|
||||
expect(roomAddress).toBe("#somealias2:aser.ver");
|
||||
done();
|
||||
};
|
||||
|
||||
dispatch({ action: Action.ViewRoom, room_alias: '#somealias2:aser.ver' });
|
||||
});
|
||||
});
|
86
test/stores/RoomViewStore-test.tsx
Normal file
86
test/stores/RoomViewStore-test.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2017 - 2022 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 { Room } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
import { RoomViewStore } from '../../src/stores/RoomViewStore';
|
||||
import { Action } from '../../src/dispatcher/actions';
|
||||
import * as testUtils from '../test-utils';
|
||||
import { flushPromises, getMockClientWithEventEmitter } from '../test-utils';
|
||||
|
||||
const dispatch = testUtils.getDispatchForStore(RoomViewStore.instance);
|
||||
|
||||
jest.mock('../../src/utils/DMRoomMap', () => {
|
||||
const mock = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
getDMRoomsForUserId: jest.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
shared: jest.fn().mockReturnValue(mock),
|
||||
sharedInstance: mock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('RoomViewStore', function() {
|
||||
const userId = '@alice:server';
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
joinRoom: jest.fn(),
|
||||
getRoom: jest.fn(),
|
||||
getRoomIdForAlias: jest.fn(),
|
||||
});
|
||||
const room = new Room('!room:server', mockClient, userId);
|
||||
|
||||
beforeEach(function() {
|
||||
jest.clearAllMocks();
|
||||
mockClient.credentials = { userId: "@test:example.com" };
|
||||
mockClient.joinRoom.mockResolvedValue(room);
|
||||
mockClient.getRoom.mockReturnValue(room);
|
||||
|
||||
// Reset the state of the store
|
||||
RoomViewStore.instance.reset();
|
||||
});
|
||||
|
||||
it('can be used to view a room by ID and join', async () => {
|
||||
dispatch({ action: Action.ViewRoom, room_id: '!randomcharacters:aser.ver' });
|
||||
dispatch({ action: 'join_room' });
|
||||
await flushPromises();
|
||||
expect(mockClient.joinRoom).toHaveBeenCalledWith('!randomcharacters:aser.ver', { viaServers: [] });
|
||||
expect(RoomViewStore.instance.isJoining()).toBe(true);
|
||||
});
|
||||
|
||||
it('can be used to view a room by alias and join', async () => {
|
||||
const roomId = "!randomcharacters:aser.ver";
|
||||
const alias = "#somealias2:aser.ver";
|
||||
|
||||
mockClient.getRoomIdForAlias.mockResolvedValue({ room_id: roomId, servers: [] });
|
||||
|
||||
dispatch({ action: Action.ViewRoom, room_alias: alias });
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
|
||||
// roomId is set to id of the room alias
|
||||
expect(RoomViewStore.instance.getRoomId()).toBe(roomId);
|
||||
|
||||
// join the room
|
||||
dispatch({ action: 'join_room' });
|
||||
|
||||
expect(RoomViewStore.instance.isJoining()).toBeTruthy();
|
||||
await flushPromises();
|
||||
|
||||
expect(mockClient.joinRoom).toHaveBeenCalledWith(alias, { viaServers: [] });
|
||||
});
|
||||
});
|
|
@ -6644,9 +6644,9 @@ mathml-tag-names@^2.1.3:
|
|||
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
|
||||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||
|
||||
"matrix-analytics-events@github:matrix-org/matrix-analytics-events.git#4aef17b56798639906f26a8739043a3c5c5fde7e":
|
||||
"matrix-analytics-events@github:matrix-org/matrix-analytics-events.git#a0687ca6fbdb7258543d49b99fb88b9201e900b0":
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-analytics-events/tar.gz/4aef17b56798639906f26a8739043a3c5c5fde7e"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-analytics-events/tar.gz/a0687ca6fbdb7258543d49b99fb88b9201e900b0"
|
||||
|
||||
matrix-encrypt-attachment@^1.0.3:
|
||||
version "1.0.3"
|
||||
|
|
Loading…
Reference in a new issue