Merge remote-tracking branch 'origin/develop' into feat/emoji-picker-rich-text-mode
This commit is contained in:
commit
1bd560d350
39 changed files with 805 additions and 125 deletions
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -1,3 +1,31 @@
|
||||||
|
Changes in [3.62.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.62.0) (2022-12-06)
|
||||||
|
=====================================================================================================
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Further improve replies ([\#6396](https://github.com/matrix-org/matrix-react-sdk/pull/6396)). Fixes vector-im/element-web#19074, vector-im/element-web#18194 vector-im/element-web#18027 and vector-im/element-web#19179.
|
||||||
|
* Enable users to join group calls from multiple devices ([\#9625](https://github.com/matrix-org/matrix-react-sdk/pull/9625)).
|
||||||
|
* fix(visual): make cursor a pointer for summaries ([\#9419](https://github.com/matrix-org/matrix-react-sdk/pull/9419)). Contributed by @r00ster91.
|
||||||
|
* Add placeholder for rich text editor ([\#9613](https://github.com/matrix-org/matrix-react-sdk/pull/9613)).
|
||||||
|
* Consolidate public room search experience ([\#9605](https://github.com/matrix-org/matrix-react-sdk/pull/9605)). Fixes vector-im/element-web#22846.
|
||||||
|
* New password reset flow ([\#9581](https://github.com/matrix-org/matrix-react-sdk/pull/9581)). Fixes vector-im/element-web#23131.
|
||||||
|
* Device manager - add tooltip to device details toggle ([\#9594](https://github.com/matrix-org/matrix-react-sdk/pull/9594)).
|
||||||
|
* sliding sync: add lazy-loading member support ([\#9530](https://github.com/matrix-org/matrix-react-sdk/pull/9530)).
|
||||||
|
* Limit formatting bar offset to top of composer ([\#9365](https://github.com/matrix-org/matrix-react-sdk/pull/9365)). Fixes vector-im/element-web#12359. Contributed by @owi92.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix issues around up arrow event edit shortcut ([\#9645](https://github.com/matrix-org/matrix-react-sdk/pull/9645)). Fixes vector-im/element-web#18497 and vector-im/element-web#18964.
|
||||||
|
* Fix search not being cleared when clicking on a result ([\#9635](https://github.com/matrix-org/matrix-react-sdk/pull/9635)). Fixes vector-im/element-web#23845.
|
||||||
|
* Fix screensharing in 1:1 calls ([\#9612](https://github.com/matrix-org/matrix-react-sdk/pull/9612)). Fixes vector-im/element-web#23808.
|
||||||
|
* Fix the background color flashing when joining a call ([\#9640](https://github.com/matrix-org/matrix-react-sdk/pull/9640)).
|
||||||
|
* Fix the size of the 'Private space' icon ([\#9638](https://github.com/matrix-org/matrix-react-sdk/pull/9638)).
|
||||||
|
* Fix reply editing in rich text editor (https ([\#9615](https://github.com/matrix-org/matrix-react-sdk/pull/9615)).
|
||||||
|
* Fix thread list jumping back down while scrolling ([\#9606](https://github.com/matrix-org/matrix-react-sdk/pull/9606)). Fixes vector-im/element-web#23727.
|
||||||
|
* Fix regression with TimelinePanel props updates not taking effect ([\#9608](https://github.com/matrix-org/matrix-react-sdk/pull/9608)). Fixes vector-im/element-web#23794.
|
||||||
|
* Fix form tooltip positioning ([\#9598](https://github.com/matrix-org/matrix-react-sdk/pull/9598)). Fixes vector-im/element-web#22861.
|
||||||
|
* Extract Search handling from RoomView into its own Component ([\#9574](https://github.com/matrix-org/matrix-react-sdk/pull/9574)). Fixes vector-im/element-web#498.
|
||||||
|
* Fix call splitbrains when switching between rooms ([\#9692](https://github.com/matrix-org/matrix-react-sdk/pull/9692)).
|
||||||
|
* Fix replies to emotes not showing as inline ([\#9707](https://github.com/matrix-org/matrix-react-sdk/pull/9707)). Fixes vector-im/element-web#23903.
|
||||||
|
|
||||||
Changes in [3.61.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.61.0) (2022-11-22)
|
Changes in [3.61.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.61.0) (2022-11-22)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.61.0",
|
"version": "3.62.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -375,4 +375,5 @@
|
||||||
@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss";
|
@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss";
|
||||||
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
|
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
|
||||||
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
|
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
|
||||||
|
@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss";
|
||||||
@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss";
|
@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss";
|
||||||
|
|
|
@ -137,15 +137,50 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
/* specialisation for password reset views */
|
/* specialisation for password reset views */
|
||||||
.mx_AuthBody_forgot-password {
|
.mx_AuthBody.mx_AuthBody_forgot-password {
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
color: $primary-content;
|
color: $primary-content;
|
||||||
padding: 50px 32px;
|
padding: 50px 32px;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-bottom: $spacing-20;
|
margin: $spacing-24 0;
|
||||||
margin-top: $spacing-24;
|
}
|
||||||
|
|
||||||
|
.mx_AuthBody_button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Login_submit {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
margin: 0 0 $spacing-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AuthBody_text {
|
||||||
|
margin-bottom: $spacing-32;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 $spacing-8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AuthBody_sign-in-instead-button {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
padding: $spacing-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AuthBody_fieldRow {
|
||||||
|
margin-bottom: $spacing-24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton.mx_AccessibleButton_hasKind {
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: .4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,12 +189,6 @@ limitations under the License.
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacing-8;
|
gap: $spacing-8;
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-top: $spacing-24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AuthBody_did-not-receive--centered {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody_resend-button {
|
.mx_AuthBody_resend-button {
|
||||||
|
@ -168,7 +197,7 @@ limitations under the License.
|
||||||
color: $accent;
|
color: $accent;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacing-4;
|
gap: $spacing-4;
|
||||||
padding: 4px;
|
padding: $spacing-4;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $system;
|
background-color: $system;
|
||||||
|
@ -209,7 +238,7 @@ limitations under the License.
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.mx_AuthBody_paddedFooter_title {
|
.mx_AuthBody_paddedFooter_title {
|
||||||
margin-top: 16px;
|
margin-top: $spacing-16;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
@ -220,7 +249,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody_paddedFooter_subtitle {
|
.mx_AuthBody_paddedFooter_subtitle {
|
||||||
margin-top: 8px;
|
margin-top: $spacing-8;
|
||||||
font-size: $font-10px;
|
font-size: $font-10px;
|
||||||
line-height: $font-14px;
|
line-height: $font-14px;
|
||||||
}
|
}
|
||||||
|
@ -236,7 +265,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SSOButtons + .mx_AuthBody_changeFlow {
|
.mx_SSOButtons + .mx_AuthBody_changeFlow {
|
||||||
margin-top: 24px;
|
margin-top: $spacing-24;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody_spinner {
|
.mx_AuthBody_spinner {
|
||||||
|
|
|
@ -20,8 +20,8 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_Dialog {
|
.mx_Dialog {
|
||||||
color: $primary-content;
|
color: $primary-content;
|
||||||
font-size: 14px;
|
font-size: $font-14px;
|
||||||
padding: 16px;
|
padding: $spacing-24 $spacing-24 $spacing-16;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 485px;
|
width: 485px;
|
||||||
|
|
||||||
|
@ -34,5 +34,14 @@ limitations under the License.
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AuthBody_did-not-receive {
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: $spacing-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Dialog_cancelButton {
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_RoomTile .mx_RoomTile_titleContainer .mx_RoomTile_subtitle.mx_RoomTile_subtitle--voice-broadcast {
|
||||||
|
align-items: center;
|
||||||
|
color: $alert;
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-4;
|
||||||
|
}
|
|
@ -48,6 +48,7 @@ import {
|
||||||
} from "./utils/device/clientInformation";
|
} from "./utils/device/clientInformation";
|
||||||
import SettingsStore, { CallbackFn } from "./settings/SettingsStore";
|
import SettingsStore, { CallbackFn } from "./settings/SettingsStore";
|
||||||
import { UIFeature } from "./settings/UIFeature";
|
import { UIFeature } from "./settings/UIFeature";
|
||||||
|
import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder";
|
||||||
|
|
||||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
@ -335,12 +336,15 @@ export default class DeviceListener {
|
||||||
logger.debug("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(','));
|
logger.debug("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(','));
|
||||||
logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(','));
|
logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(','));
|
||||||
|
|
||||||
|
const isBulkUnverifiedSessionsReminderSnoozed = isBulkUnverifiedDeviceReminderSnoozed();
|
||||||
|
|
||||||
// Display or hide the batch toast for old unverified sessions
|
// Display or hide the batch toast for old unverified sessions
|
||||||
// don't show the toast if the current device is unverified
|
// don't show the toast if the current device is unverified
|
||||||
if (
|
if (
|
||||||
oldUnverifiedDeviceIds.size > 0
|
oldUnverifiedDeviceIds.size > 0
|
||||||
&& isCurrentDeviceTrusted
|
&& isCurrentDeviceTrusted
|
||||||
&& this.enableBulkUnverifiedSessionsReminder
|
&& this.enableBulkUnverifiedSessionsReminder
|
||||||
|
&& !isBulkUnverifiedSessionsReminderSnoozed
|
||||||
) {
|
) {
|
||||||
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
|
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -347,7 +347,11 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{ this.staticModal.elem }
|
{ this.staticModal.elem }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.onBackgroundClick} />
|
<div
|
||||||
|
data-testid="dialog-background"
|
||||||
|
className="mx_Dialog_background mx_Dialog_staticBackground"
|
||||||
|
onClick={this.onBackgroundClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -368,7 +372,11 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{ modal.elem }
|
{ modal.elem }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
|
<div
|
||||||
|
data-testid="dialog-background"
|
||||||
|
className="mx_Dialog_background"
|
||||||
|
onClick={this.onBackgroundClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,6 @@ import { createClient, IRequestTokenResponse, MatrixClient } from 'matrix-js-sdk
|
||||||
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
const CHECK_EMAIL_VERIFIED_POLL_INTERVAL = 2000;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows a user to reset their password on a homeserver.
|
* Allows a user to reset their password on a homeserver.
|
||||||
*
|
*
|
||||||
|
@ -108,24 +106,6 @@ export default class PasswordReset {
|
||||||
await this.checkEmailLinkClicked();
|
await this.checkEmailLinkClicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async retrySetNewPassword(password: string): Promise<void> {
|
|
||||||
this.password = password;
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.tryCheckEmailLinkClicked(resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private tryCheckEmailLinkClicked(resolve: Function): void {
|
|
||||||
this.checkEmailLinkClicked()
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(() => {
|
|
||||||
window.setTimeout(
|
|
||||||
() => this.tryCheckEmailLinkClicked(resolve),
|
|
||||||
CHECK_EMAIL_VERIFIED_POLL_INTERVAL,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the email link has been clicked by attempting to change the password
|
* Checks if the email link has been clicked by attempting to change the password
|
||||||
* for the mxid linked to the email.
|
* for the mxid linked to the email.
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { logger } from 'matrix-js-sdk/src/logger';
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { sleep } from 'matrix-js-sdk/src/utils';
|
||||||
|
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
|
@ -43,6 +44,8 @@ import Spinner from '../../views/elements/Spinner';
|
||||||
import { formatSeconds } from '../../../DateUtils';
|
import { formatSeconds } from '../../../DateUtils';
|
||||||
import AutoDiscoveryUtils from '../../../utils/AutoDiscoveryUtils';
|
import AutoDiscoveryUtils from '../../../utils/AutoDiscoveryUtils';
|
||||||
|
|
||||||
|
const emailCheckInterval = 2000;
|
||||||
|
|
||||||
enum Phase {
|
enum Phase {
|
||||||
// Show email input
|
// Show email input
|
||||||
EnterEmail = 1,
|
EnterEmail = 1,
|
||||||
|
@ -60,7 +63,7 @@ enum Phase {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverConfig: ValidatedServerConfig;
|
serverConfig: ValidatedServerConfig;
|
||||||
onLoginClick?: () => void;
|
onLoginClick: () => void;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,22 +280,43 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
||||||
{
|
{
|
||||||
email: this.state.email,
|
email: this.state.email,
|
||||||
errorText: this.state.errorText,
|
errorText: this.state.errorText,
|
||||||
|
onCloseClick: () => {
|
||||||
|
modal.close();
|
||||||
|
this.setState({ phase: Phase.PasswordInput });
|
||||||
|
},
|
||||||
|
onReEnterEmailClick: () => {
|
||||||
|
modal.close();
|
||||||
|
this.setState({ phase: Phase.EnterEmail });
|
||||||
|
},
|
||||||
onResendClick: this.sendVerificationMail,
|
onResendClick: this.sendVerificationMail,
|
||||||
},
|
},
|
||||||
"mx_VerifyEMailDialog",
|
"mx_VerifyEMailDialog",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
{
|
{
|
||||||
// this modal cannot be dismissed except reset is done or forced
|
|
||||||
onBeforeClose: async (reason?: string) => {
|
onBeforeClose: async (reason?: string) => {
|
||||||
return this.state.phase === Phase.Done || reason === "force";
|
if (reason === "backgroundClick") {
|
||||||
|
// Modal dismissed by clicking the background.
|
||||||
|
// Go one phase back.
|
||||||
|
this.setState({ phase: Phase.PasswordInput });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.reset.retrySetNewPassword(this.state.password);
|
// Don't retry if the phase changed. For example when going back to email input.
|
||||||
this.phase = Phase.Done;
|
while (this.state.phase === Phase.ResettingPassword) {
|
||||||
|
try {
|
||||||
|
await this.reset.setNewPassword(this.state.password);
|
||||||
|
this.setState({ phase: Phase.Done });
|
||||||
modal.close();
|
modal.close();
|
||||||
|
} catch (e) {
|
||||||
|
// Email not confirmed, yet. Retry after a while.
|
||||||
|
await sleep(emailCheckInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSubmitForm = async (ev: React.FormEvent): Promise<void> => {
|
private onSubmitForm = async (ev: React.FormEvent): Promise<void> => {
|
||||||
|
@ -339,6 +363,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
||||||
homeserver={this.props.serverConfig.hsName}
|
homeserver={this.props.serverConfig.hsName}
|
||||||
loading={this.state.phase === Phase.SendingEmail}
|
loading={this.state.phase === Phase.SendingEmail}
|
||||||
onInputChanged={this.onInputChanged}
|
onInputChanged={this.onInputChanged}
|
||||||
|
onLoginClick={this.props.onLoginClick!} // set by default props
|
||||||
onSubmitForm={this.onSubmitForm}
|
onSubmitForm={this.onSubmitForm}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
@ -374,6 +399,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
||||||
return <CheckEmail
|
return <CheckEmail
|
||||||
email={this.state.email}
|
email={this.state.email}
|
||||||
errorText={this.state.errorText}
|
errorText={this.state.errorText}
|
||||||
|
onReEnterEmailClick={() => this.setState({ phase: Phase.EnterEmail })}
|
||||||
onResendClick={this.sendVerificationMail}
|
onResendClick={this.sendVerificationMail}
|
||||||
onSubmitForm={this.onSubmitForm}
|
onSubmitForm={this.onSubmitForm}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { ErrorMessage } from "../../ErrorMessage";
|
||||||
interface CheckEmailProps {
|
interface CheckEmailProps {
|
||||||
email: string;
|
email: string;
|
||||||
errorText: string | ReactNode | null;
|
errorText: string | ReactNode | null;
|
||||||
|
onReEnterEmailClick: () => void;
|
||||||
onResendClick: () => Promise<boolean>;
|
onResendClick: () => Promise<boolean>;
|
||||||
onSubmitForm: (ev: React.FormEvent) => void;
|
onSubmitForm: (ev: React.FormEvent) => void;
|
||||||
}
|
}
|
||||||
|
@ -37,6 +38,7 @@ interface CheckEmailProps {
|
||||||
export const CheckEmail: React.FC<CheckEmailProps> = ({
|
export const CheckEmail: React.FC<CheckEmailProps> = ({
|
||||||
email,
|
email,
|
||||||
errorText,
|
errorText,
|
||||||
|
onReEnterEmailClick,
|
||||||
onSubmitForm,
|
onSubmitForm,
|
||||||
onResendClick,
|
onResendClick,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -50,6 +52,7 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
||||||
return <>
|
return <>
|
||||||
<EMailPromptIcon className="mx_AuthBody_emailPromptIcon--shifted" />
|
<EMailPromptIcon className="mx_AuthBody_emailPromptIcon--shifted" />
|
||||||
<h1>{ _t("Check your email to continue") }</h1>
|
<h1>{ _t("Check your email to continue") }</h1>
|
||||||
|
<div className="mx_AuthBody_text">
|
||||||
<p>
|
<p>
|
||||||
{ _t(
|
{ _t(
|
||||||
"Follow the instructions sent to <b>%(email)s</b>",
|
"Follow the instructions sent to <b>%(email)s</b>",
|
||||||
|
@ -57,6 +60,24 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
||||||
{ b: t => <b>{ t }</b> },
|
{ b: t => <b>{ t }</b> },
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mx_AuthBody_did-not-receive">
|
||||||
|
<span className="mx_VerifyEMailDialog_text-light">{ _t("Wrong email address?") }</span>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_AuthBody_resend-button"
|
||||||
|
kind="link"
|
||||||
|
onClick={onReEnterEmailClick}
|
||||||
|
>
|
||||||
|
{ _t("Re-enter email address") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ errorText && <ErrorMessage message={errorText} /> }
|
||||||
|
<input
|
||||||
|
onClick={onSubmitForm}
|
||||||
|
type="button"
|
||||||
|
className="mx_Login_submit"
|
||||||
|
value={_t("Next")}
|
||||||
|
/>
|
||||||
<div className="mx_AuthBody_did-not-receive">
|
<div className="mx_AuthBody_did-not-receive">
|
||||||
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
@ -73,12 +94,5 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
||||||
/>
|
/>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
{ errorText && <ErrorMessage message={errorText} /> }
|
|
||||||
<input
|
|
||||||
onClick={onSubmitForm}
|
|
||||||
type="button"
|
|
||||||
className="mx_Login_submit"
|
|
||||||
value={_t("Next")}
|
|
||||||
/>
|
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,6 +22,7 @@ import EmailField from "../../../views/auth/EmailField";
|
||||||
import { ErrorMessage } from "../../ErrorMessage";
|
import { ErrorMessage } from "../../ErrorMessage";
|
||||||
import Spinner from "../../../views/elements/Spinner";
|
import Spinner from "../../../views/elements/Spinner";
|
||||||
import Field from "../../../views/elements/Field";
|
import Field from "../../../views/elements/Field";
|
||||||
|
import AccessibleButton from "../../../views/elements/AccessibleButton";
|
||||||
|
|
||||||
interface EnterEmailProps {
|
interface EnterEmailProps {
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -29,6 +30,7 @@ interface EnterEmailProps {
|
||||||
homeserver: string;
|
homeserver: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onInputChanged: (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => void;
|
onInputChanged: (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => void;
|
||||||
|
onLoginClick: () => void;
|
||||||
onSubmitForm: (ev: React.FormEvent) => void;
|
onSubmitForm: (ev: React.FormEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +43,7 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
||||||
homeserver,
|
homeserver,
|
||||||
loading,
|
loading,
|
||||||
onInputChanged,
|
onInputChanged,
|
||||||
|
onLoginClick,
|
||||||
onSubmitForm,
|
onSubmitForm,
|
||||||
}) => {
|
}) => {
|
||||||
const submitButtonChild = loading
|
const submitButtonChild = loading
|
||||||
|
@ -92,6 +95,15 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
||||||
>
|
>
|
||||||
{ submitButtonChild }
|
{ submitButtonChild }
|
||||||
</button>
|
</button>
|
||||||
|
<div className="mx_AuthBody_button-container">
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_AuthBody_sign-in-instead-button"
|
||||||
|
element="button"
|
||||||
|
kind="link"
|
||||||
|
onClick={onLoginClick}>
|
||||||
|
{ _t("Sign in instead") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</>;
|
</>;
|
||||||
|
|
|
@ -27,12 +27,16 @@ import { ErrorMessage } from "../../ErrorMessage";
|
||||||
interface Props {
|
interface Props {
|
||||||
email: string;
|
email: string;
|
||||||
errorText: string | null;
|
errorText: string | null;
|
||||||
|
onCloseClick: () => void;
|
||||||
|
onReEnterEmailClick: () => void;
|
||||||
onResendClick: () => Promise<boolean>;
|
onResendClick: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VerifyEmailModal: React.FC<Props> = ({
|
export const VerifyEmailModal: React.FC<Props> = ({
|
||||||
email,
|
email,
|
||||||
errorText,
|
errorText,
|
||||||
|
onCloseClick,
|
||||||
|
onReEnterEmailClick,
|
||||||
onResendClick,
|
onResendClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500);
|
const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500);
|
||||||
|
@ -57,7 +61,8 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
||||||
},
|
},
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<div className="mx_AuthBody_did-not-receive mx_AuthBody_did-not-receive--centered">
|
|
||||||
|
<div className="mx_AuthBody_did-not-receive">
|
||||||
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_AuthBody_resend-button"
|
className="mx_AuthBody_resend-button"
|
||||||
|
@ -74,5 +79,22 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{ errorText && <ErrorMessage message={errorText} /> }
|
{ errorText && <ErrorMessage message={errorText} /> }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_AuthBody_did-not-receive">
|
||||||
|
<span className="mx_VerifyEMailDialog_text-light">{ _t("Wrong email address?") }</span>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_AuthBody_resend-button"
|
||||||
|
kind="link"
|
||||||
|
onClick={onReEnterEmailClick}
|
||||||
|
>
|
||||||
|
{ _t("Re-enter email address") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={onCloseClick}
|
||||||
|
className="mx_Dialog_cancelButton"
|
||||||
|
aria-label={_t("Close dialog")}
|
||||||
|
/>
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,7 @@ import classNames from "classnames";
|
||||||
import { formatCount } from "../../../../utils/FormattingUtils";
|
import { formatCount } from "../../../../utils/FormattingUtils";
|
||||||
import AccessibleButton from "../../elements/AccessibleButton";
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
import { NotificationColor } from "../../../../stores/notifications/NotificationColor";
|
import { NotificationColor } from "../../../../stores/notifications/NotificationColor";
|
||||||
|
import { useSettingValue } from "../../../../hooks/useSettings";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
symbol: string | null;
|
symbol: string | null;
|
||||||
|
@ -37,8 +38,12 @@ export function StatelessNotificationBadge({
|
||||||
count,
|
count,
|
||||||
color,
|
color,
|
||||||
...props }: Props) {
|
...props }: Props) {
|
||||||
|
const hideBold = useSettingValue("feature_hidebold");
|
||||||
|
|
||||||
// Don't show a badge if we don't need to
|
// Don't show a badge if we don't need to
|
||||||
if (color === NotificationColor.None) return null;
|
if (color === NotificationColor.None || (hideBold && color == NotificationColor.Bold)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol);
|
const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol);
|
||||||
|
|
||||||
|
|
|
@ -48,20 +48,25 @@ import { RoomTileCallSummary } from "./RoomTileCallSummary";
|
||||||
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
||||||
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
|
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
|
import { useHasRoomLiveVoiceBroadcast, VoiceBroadcastRoomSubtitle } from "../../../voice-broadcast";
|
||||||
|
|
||||||
interface IProps {
|
interface Props {
|
||||||
room: Room;
|
room: Room;
|
||||||
showMessagePreview: boolean;
|
showMessagePreview: boolean;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
tag: TagID;
|
tag: TagID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ClassProps extends Props {
|
||||||
|
hasLiveVoiceBroadcast: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
||||||
|
|
||||||
interface IState {
|
interface State {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
notificationsMenuPosition: PartialDOMRect;
|
notificationsMenuPosition: PartialDOMRect | null;
|
||||||
generalMenuPosition: PartialDOMRect;
|
generalMenuPosition: PartialDOMRect | null;
|
||||||
call: Call | null;
|
call: Call | null;
|
||||||
messagePreview?: string;
|
messagePreview?: string;
|
||||||
}
|
}
|
||||||
|
@ -76,13 +81,13 @@ export const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
||||||
return { left, top, chevronFace };
|
return { left, top, chevronFace };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class RoomTile extends React.PureComponent<IProps, IState> {
|
export class RoomTile extends React.PureComponent<ClassProps, State> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef?: string;
|
||||||
private roomTileRef = createRef<HTMLDivElement>();
|
private roomTileRef = createRef<HTMLDivElement>();
|
||||||
private notificationState: NotificationState;
|
private notificationState: NotificationState;
|
||||||
private roomProps: RoomEchoChamber;
|
private roomProps: RoomEchoChamber;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: ClassProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -120,7 +125,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
return !this.props.isMinimized && this.props.showMessagePreview;
|
return !this.props.isMinimized && this.props.showMessagePreview;
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
|
public componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
||||||
const showMessageChanged = prevProps.showMessagePreview !== this.props.showMessagePreview;
|
const showMessageChanged = prevProps.showMessagePreview !== this.props.showMessagePreview;
|
||||||
const minimizedChanged = prevProps.isMinimized !== this.props.isMinimized;
|
const minimizedChanged = prevProps.isMinimized !== this.props.isMinimized;
|
||||||
if (showMessageChanged || minimizedChanged) {
|
if (showMessageChanged || minimizedChanged) {
|
||||||
|
@ -169,7 +174,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.onRoomPreviewChanged,
|
this.onRoomPreviewChanged,
|
||||||
);
|
);
|
||||||
this.props.room.off(RoomEvent.Name, this.onRoomNameUpdate);
|
this.props.room.off(RoomEvent.Name, this.onRoomNameUpdate);
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||||
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||||
CallStore.instance.off(CallStoreEvent.Call, this.onCallChanged);
|
CallStore.instance.off(CallStoreEvent.Call, this.onCallChanged);
|
||||||
|
@ -218,12 +223,14 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||||
|
const clearSearch = ([KeyBindingAction.Enter, KeyBindingAction.Space] as Array<string | undefined>)
|
||||||
|
.includes(action);
|
||||||
|
|
||||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
show_room_tile: true, // make sure the room is visible in the list
|
show_room_tile: true, // make sure the room is visible in the list
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
clear_search: [KeyBindingAction.Enter, KeyBindingAction.Space].includes(action),
|
clear_search: clearSearch,
|
||||||
metricsTrigger: "RoomList",
|
metricsTrigger: "RoomList",
|
||||||
metricsViaKeyboard: ev.type !== "click",
|
metricsViaKeyboard: ev.type !== "click",
|
||||||
});
|
});
|
||||||
|
@ -233,7 +240,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.setState({ selected: isActive });
|
this.setState({ selected: isActive });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onNotificationsMenuOpenClick = (ev: React.MouseEvent) => {
|
private onNotificationsMenuOpenClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.target as HTMLButtonElement;
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
@ -246,7 +253,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.setState({ notificationsMenuPosition: null });
|
this.setState({ notificationsMenuPosition: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onGeneralMenuOpenClick = (ev: React.MouseEvent) => {
|
private onGeneralMenuOpenClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.target as HTMLButtonElement;
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
@ -271,7 +278,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.setState({ generalMenuPosition: null });
|
this.setState({ generalMenuPosition: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
|
private renderNotificationsMenu(isActive: boolean): React.ReactElement | null {
|
||||||
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
|
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
|
||||||
!this.showContextMenu || this.props.isMinimized
|
!this.showContextMenu || this.props.isMinimized
|
||||||
) {
|
) {
|
||||||
|
@ -313,7 +320,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderGeneralMenu(): React.ReactElement {
|
private renderGeneralMenu(): React.ReactElement | null {
|
||||||
if (!this.showContextMenu) return null; // no menu to show
|
if (!this.showContextMenu) return null; // no menu to show
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -379,6 +386,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
<RoomTileCallSummary call={this.state.call} />
|
<RoomTileCallSummary call={this.state.call} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (this.props.hasLiveVoiceBroadcast) {
|
||||||
|
subtitle = <VoiceBroadcastRoomSubtitle />;
|
||||||
} else if (this.showMessagePreview && this.state.messagePreview) {
|
} else if (this.showMessagePreview && this.state.messagePreview) {
|
||||||
subtitle = (
|
subtitle = (
|
||||||
<div
|
<div
|
||||||
|
@ -472,3 +481,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RoomTileHOC: React.FC<Props> = (props: Props) => {
|
||||||
|
const hasLiveVoiceBroadcast = useHasRoomLiveVoiceBroadcast(props.room);
|
||||||
|
return <RoomTile {...props} hasLiveVoiceBroadcast={hasLiveVoiceBroadcast} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomTileHOC;
|
||||||
|
|
|
@ -960,6 +960,7 @@
|
||||||
"Show stickers button": "Show stickers button",
|
"Show stickers button": "Show stickers button",
|
||||||
"Show polls button": "Show polls button",
|
"Show polls button": "Show polls button",
|
||||||
"Insert a trailing colon after user mentions at the start of a message": "Insert a trailing colon after user mentions at the start of a message",
|
"Insert a trailing colon after user mentions at the start of a message": "Insert a trailing colon after user mentions at the start of a message",
|
||||||
|
"Hide notification dot (only display counters badges)": "Hide notification dot (only display counters badges)",
|
||||||
"Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout",
|
"Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout",
|
||||||
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
||||||
"Show join/leave messages (invites/removes/bans unaffected)": "Show join/leave messages (invites/removes/bans unaffected)",
|
"Show join/leave messages (invites/removes/bans unaffected)": "Show join/leave messages (invites/removes/bans unaffected)",
|
||||||
|
@ -3496,6 +3497,8 @@
|
||||||
"Clear personal data": "Clear personal data",
|
"Clear personal data": "Clear personal data",
|
||||||
"Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.",
|
"Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.",
|
||||||
"Follow the instructions sent to <b>%(email)s</b>": "Follow the instructions sent to <b>%(email)s</b>",
|
"Follow the instructions sent to <b>%(email)s</b>": "Follow the instructions sent to <b>%(email)s</b>",
|
||||||
|
"Wrong email address?": "Wrong email address?",
|
||||||
|
"Re-enter email address": "Re-enter email address",
|
||||||
"Did not receive it?": "Did not receive it?",
|
"Did not receive it?": "Did not receive it?",
|
||||||
"Verification link email resent!": "Verification link email resent!",
|
"Verification link email resent!": "Verification link email resent!",
|
||||||
"Send email": "Send email",
|
"Send email": "Send email",
|
||||||
|
@ -3503,6 +3506,7 @@
|
||||||
"<b>%(homeserver)s</b> will send you a verification link to let you reset your password.": "<b>%(homeserver)s</b> will send you a verification link to let you reset your password.",
|
"<b>%(homeserver)s</b> will send you a verification link to let you reset your password.": "<b>%(homeserver)s</b> will send you a verification link to let you reset your password.",
|
||||||
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
|
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
|
||||||
"The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.",
|
"The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.",
|
||||||
|
"Sign in instead": "Sign in instead",
|
||||||
"Verify your email to continue": "Verify your email to continue",
|
"Verify your email to continue": "Verify your email to continue",
|
||||||
"We need to know it’s you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>": "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>",
|
"We need to know it’s you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>": "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>",
|
||||||
"Commands": "Commands",
|
"Commands": "Commands",
|
||||||
|
|
|
@ -556,11 +556,18 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_hidebold": {
|
||||||
|
isFeature: true,
|
||||||
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||||
|
displayName: _td("Hide notification dot (only display counters badges)"),
|
||||||
|
labsGroup: LabGroup.Rooms,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"useCompactLayout": {
|
"useCompactLayout": {
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
displayName: _td("Use a more compact 'Modern' layout"),
|
displayName: _td("Use a more compact 'Modern' layout"),
|
||||||
default: false,
|
default: false,
|
||||||
controller: new IncompatibleController("layout", false, v => v !== Layout.Group),
|
controller: new IncompatibleController("layout", false, (v: Layout) => v !== Layout.Group),
|
||||||
},
|
},
|
||||||
"showRedactions": {
|
"showRedactions": {
|
||||||
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class ListNotificationState extends NotificationState {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get symbol(): string {
|
public get symbol(): string | null {
|
||||||
return this._color === NotificationColor.Unsent ? "!" : null;
|
return this._color === NotificationColor.Unsent ? "!" : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter"
|
||||||
|
|
||||||
import { NotificationColor } from "./NotificationColor";
|
import { NotificationColor } from "./NotificationColor";
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
import { IDestroyable } from "../../utils/IDestroyable";
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
export interface INotificationStateSnapshotParams {
|
export interface INotificationStateSnapshotParams {
|
||||||
symbol: string | null;
|
symbol: string | null;
|
||||||
|
@ -37,11 +38,22 @@ export abstract class NotificationState
|
||||||
extends TypedEventEmitter<NotificationStateEvents, EventHandlerMap>
|
extends TypedEventEmitter<NotificationStateEvents, EventHandlerMap>
|
||||||
implements INotificationStateSnapshotParams, IDestroyable {
|
implements INotificationStateSnapshotParams, IDestroyable {
|
||||||
//
|
//
|
||||||
protected _symbol: string | null;
|
protected _symbol: string | null = null;
|
||||||
protected _count: number;
|
protected _count = 0;
|
||||||
protected _color: NotificationColor;
|
protected _color: NotificationColor = NotificationColor.None;
|
||||||
|
|
||||||
public get symbol(): string {
|
private watcherReferences: string[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.watcherReferences.push(
|
||||||
|
SettingsStore.watchSetting("feature_hidebold", null, () => {
|
||||||
|
this.emit(NotificationStateEvents.Update);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get symbol(): string | null {
|
||||||
return this._symbol;
|
return this._symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +70,12 @@ export abstract class NotificationState
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isUnread(): boolean {
|
public get isUnread(): boolean {
|
||||||
return this.color >= NotificationColor.Bold;
|
if (this.color > NotificationColor.Bold) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const hideBold = SettingsStore.getValue("feature_hidebold");
|
||||||
|
return this.color === NotificationColor.Bold && !hideBold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get hasUnreadCount(): boolean {
|
public get hasUnreadCount(): boolean {
|
||||||
|
@ -81,11 +98,15 @@ export abstract class NotificationState
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
this.removeAllListeners(NotificationStateEvents.Update);
|
this.removeAllListeners(NotificationStateEvents.Update);
|
||||||
|
for (const watcherReference of this.watcherReferences) {
|
||||||
|
SettingsStore.unwatchSetting(watcherReference);
|
||||||
|
}
|
||||||
|
this.watcherReferences = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotificationStateSnapshot {
|
export class NotificationStateSnapshot {
|
||||||
private readonly symbol: string;
|
private readonly symbol: string | null;
|
||||||
private readonly count: number;
|
private readonly count: number;
|
||||||
private readonly color: NotificationColor;
|
private readonly color: NotificationColor;
|
||||||
|
|
||||||
|
|
|
@ -98,8 +98,8 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleRoomEventUpdate = (event: MatrixEvent, room: Room | null) => {
|
private handleRoomEventUpdate = (event: MatrixEvent) => {
|
||||||
if (room?.roomId !== this.room.roomId) return; // ignore - not for us or notifications timeline
|
if (event?.getRoomId() !== this.room.roomId) return; // ignore - not for us or notifications timeline
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class SpaceNotificationState extends NotificationState {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get symbol(): string {
|
public get symbol(): string | null {
|
||||||
return this._color === NotificationColor.Unsent ? "!" : null;
|
return this._color === NotificationColor.Unsent ? "!" : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { NotificationState } from "./NotificationState";
|
||||||
export class StaticNotificationState extends NotificationState {
|
export class StaticNotificationState extends NotificationState {
|
||||||
public static readonly RED_EXCLAMATION = StaticNotificationState.forSymbol("!", NotificationColor.Red);
|
public static readonly RED_EXCLAMATION = StaticNotificationState.forSymbol("!", NotificationColor.Red);
|
||||||
|
|
||||||
constructor(symbol: string, count: number, color: NotificationColor) {
|
constructor(symbol: string | null, count: number, color: NotificationColor) {
|
||||||
super();
|
super();
|
||||||
this._symbol = symbol;
|
this._symbol = symbol;
|
||||||
this._count = count;
|
this._count = count;
|
||||||
|
|
|
@ -20,6 +20,7 @@ import DeviceListener from '../DeviceListener';
|
||||||
import GenericToast from "../components/views/toasts/GenericToast";
|
import GenericToast from "../components/views/toasts/GenericToast";
|
||||||
import ToastStore from "../stores/ToastStore";
|
import ToastStore from "../stores/ToastStore";
|
||||||
import { Action } from "../dispatcher/actions";
|
import { Action } from "../dispatcher/actions";
|
||||||
|
import { snoozeBulkUnverifiedDeviceReminder } from '../utils/device/snoozeBulkUnverifiedDeviceReminder';
|
||||||
|
|
||||||
const TOAST_KEY = "reviewsessions";
|
const TOAST_KEY = "reviewsessions";
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ export const showToast = (deviceIds: Set<string>) => {
|
||||||
|
|
||||||
const onReject = () => {
|
const onReject = () => {
|
||||||
DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds);
|
DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds);
|
||||||
|
snoozeBulkUnverifiedDeviceReminder();
|
||||||
};
|
};
|
||||||
|
|
||||||
ToastStore.sharedInstance().addOrReplaceToast({
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
|
|
40
src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts
Normal file
40
src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
const SNOOZE_KEY = 'mx_snooze_bulk_unverified_device_nag';
|
||||||
|
// one week
|
||||||
|
const snoozePeriod = 1000 * 60 * 60 * 24 * 7;
|
||||||
|
export const snoozeBulkUnverifiedDeviceReminder = () => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(SNOOZE_KEY, String(Date.now()));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to persist bulk unverified device nag snooze', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isBulkUnverifiedDeviceReminderSnoozed = () => {
|
||||||
|
try {
|
||||||
|
const snoozedTimestamp = localStorage.getItem(SNOOZE_KEY);
|
||||||
|
|
||||||
|
const parsedTimestamp = Number.parseInt(snoozedTimestamp || '', 10);
|
||||||
|
|
||||||
|
return Number.isInteger(parsedTimestamp) && (parsedTimestamp + snoozePeriod) > Date.now();
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
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 { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
|
export const VoiceBroadcastRoomSubtitle = () => {
|
||||||
|
return <div className="mx_RoomTile_subtitle mx_RoomTile_subtitle--voice-broadcast">
|
||||||
|
<LiveIcon className="mx_Icon mx_Icon_16" />
|
||||||
|
{ _t("Live") }
|
||||||
|
</div>;
|
||||||
|
};
|
35
src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts
Normal file
35
src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
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 { useState } from "react";
|
||||||
|
import { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { hasRoomLiveVoiceBroadcast } from "../utils/hasRoomLiveVoiceBroadcast";
|
||||||
|
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
|
||||||
|
|
||||||
|
export const useHasRoomLiveVoiceBroadcast = (room: Room) => {
|
||||||
|
const [hasLiveVoiceBroadcast, setHasLiveVoiceBroadcast] = useState(hasRoomLiveVoiceBroadcast(room).hasBroadcast);
|
||||||
|
|
||||||
|
useTypedEventEmitter(
|
||||||
|
room.currentState,
|
||||||
|
RoomStateEvent.Update,
|
||||||
|
() => {
|
||||||
|
setHasLiveVoiceBroadcast(hasRoomLiveVoiceBroadcast(room).hasBroadcast);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return hasLiveVoiceBroadcast;
|
||||||
|
};
|
|
@ -29,12 +29,14 @@ export * from "./components/VoiceBroadcastBody";
|
||||||
export * from "./components/atoms/LiveBadge";
|
export * from "./components/atoms/LiveBadge";
|
||||||
export * from "./components/atoms/VoiceBroadcastControl";
|
export * from "./components/atoms/VoiceBroadcastControl";
|
||||||
export * from "./components/atoms/VoiceBroadcastHeader";
|
export * from "./components/atoms/VoiceBroadcastHeader";
|
||||||
|
export * from "./components/atoms/VoiceBroadcastRoomSubtitle";
|
||||||
export * from "./components/molecules/VoiceBroadcastPlaybackBody";
|
export * from "./components/molecules/VoiceBroadcastPlaybackBody";
|
||||||
export * from "./components/molecules/VoiceBroadcastPreRecordingPip";
|
export * from "./components/molecules/VoiceBroadcastPreRecordingPip";
|
||||||
export * from "./components/molecules/VoiceBroadcastRecordingBody";
|
export * from "./components/molecules/VoiceBroadcastRecordingBody";
|
||||||
export * from "./components/molecules/VoiceBroadcastRecordingPip";
|
export * from "./components/molecules/VoiceBroadcastRecordingPip";
|
||||||
export * from "./hooks/useCurrentVoiceBroadcastPreRecording";
|
export * from "./hooks/useCurrentVoiceBroadcastPreRecording";
|
||||||
export * from "./hooks/useCurrentVoiceBroadcastRecording";
|
export * from "./hooks/useCurrentVoiceBroadcastRecording";
|
||||||
|
export * from "./hooks/useHasRoomLiveVoiceBroadcast";
|
||||||
export * from "./hooks/useVoiceBroadcastRecording";
|
export * from "./hooks/useVoiceBroadcastRecording";
|
||||||
export * from "./stores/VoiceBroadcastPlaybacksStore";
|
export * from "./stores/VoiceBroadcastPlaybacksStore";
|
||||||
export * from "./stores/VoiceBroadcastPreRecordingStore";
|
export * from "./stores/VoiceBroadcastPreRecordingStore";
|
||||||
|
|
|
@ -35,6 +35,7 @@ import SettingsStore from "../src/settings/SettingsStore";
|
||||||
import { SettingLevel } from "../src/settings/SettingLevel";
|
import { SettingLevel } from "../src/settings/SettingLevel";
|
||||||
import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils";
|
import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils";
|
||||||
import { UIFeature } from "../src/settings/UIFeature";
|
import { UIFeature } from "../src/settings/UIFeature";
|
||||||
|
import { isBulkUnverifiedDeviceReminderSnoozed } from "../src/utils/device/snoozeBulkUnverifiedDeviceReminder";
|
||||||
|
|
||||||
// don't litter test console with logs
|
// don't litter test console with logs
|
||||||
jest.mock("matrix-js-sdk/src/logger");
|
jest.mock("matrix-js-sdk/src/logger");
|
||||||
|
@ -48,6 +49,10 @@ jest.mock("../src/SecurityManager", () => ({
|
||||||
isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(),
|
isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock("../src/utils/device/snoozeBulkUnverifiedDeviceReminder", () => ({
|
||||||
|
isBulkUnverifiedDeviceReminderSnoozed: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const userId = '@user:server';
|
const userId = '@user:server';
|
||||||
const deviceId = 'my-device-id';
|
const deviceId = 'my-device-id';
|
||||||
const mockDispatcher = mocked(dis);
|
const mockDispatcher = mocked(dis);
|
||||||
|
@ -95,6 +100,7 @@ describe('DeviceListener', () => {
|
||||||
});
|
});
|
||||||
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient);
|
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient);
|
||||||
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
|
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
|
||||||
|
mocked(isBulkUnverifiedDeviceReminderSnoozed).mockClear().mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
const createAndStart = async (): Promise<DeviceListener> => {
|
const createAndStart = async (): Promise<DeviceListener> => {
|
||||||
|
@ -451,6 +457,23 @@ describe('DeviceListener', () => {
|
||||||
expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled();
|
expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides toast when reminder is snoozed', async () => {
|
||||||
|
mocked(isBulkUnverifiedDeviceReminderSnoozed).mockReturnValue(true);
|
||||||
|
// currentDevice, device2 are verified, device3 is unverified
|
||||||
|
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {
|
||||||
|
switch (deviceId) {
|
||||||
|
case currentDevice.deviceId:
|
||||||
|
case device2.deviceId:
|
||||||
|
return deviceTrustVerified;
|
||||||
|
default:
|
||||||
|
return deviceTrustUnverified;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await createAndStart();
|
||||||
|
expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled();
|
||||||
|
expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('shows toast with unverified devices at app start', async () => {
|
it('shows toast with unverified devices at app start', async () => {
|
||||||
// currentDevice, device2 are verified, device3 is unverified
|
// currentDevice, device2 are verified, device3 is unverified
|
||||||
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {
|
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {
|
||||||
|
|
|
@ -38,6 +38,7 @@ describe("<ForgotPassword>", () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
let serverConfig: ValidatedServerConfig;
|
let serverConfig: ValidatedServerConfig;
|
||||||
let onComplete: () => void;
|
let onComplete: () => void;
|
||||||
|
let onLoginClick: () => void;
|
||||||
let renderResult: RenderResult;
|
let renderResult: RenderResult;
|
||||||
let restoreConsole: () => void;
|
let restoreConsole: () => void;
|
||||||
|
|
||||||
|
@ -49,9 +50,16 @@ describe("<ForgotPassword>", () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitForm = async (submitLabel: string): Promise<void> => {
|
const clickButton = async (label: string): Promise<void> => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await userEvent.click(screen.getByText(submitLabel), { delay: null });
|
await userEvent.click(screen.getByText(label), { delay: null });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const itShouldCloseTheDialogAndShowThePasswordInput = (): void => {
|
||||||
|
it("should close the dialog and show the password input", () => {
|
||||||
|
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Reset your password")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -70,6 +78,7 @@ describe("<ForgotPassword>", () => {
|
||||||
serverConfig.hsName = "example.com";
|
serverConfig.hsName = "example.com";
|
||||||
|
|
||||||
onComplete = jest.fn();
|
onComplete = jest.fn();
|
||||||
|
onLoginClick = jest.fn();
|
||||||
|
|
||||||
jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig);
|
jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig);
|
||||||
jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError");
|
jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError");
|
||||||
|
@ -94,6 +103,7 @@ describe("<ForgotPassword>", () => {
|
||||||
renderResult = render(<ForgotPassword
|
renderResult = render(<ForgotPassword
|
||||||
serverConfig={serverConfig}
|
serverConfig={serverConfig}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
|
onLoginClick={onLoginClick}
|
||||||
/>);
|
/>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,6 +118,7 @@ describe("<ForgotPassword>", () => {
|
||||||
renderResult.rerender(<ForgotPassword
|
renderResult.rerender(<ForgotPassword
|
||||||
serverConfig={serverConfig}
|
serverConfig={serverConfig}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
|
onLoginClick={onLoginClick}
|
||||||
/>);
|
/>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -116,6 +127,16 @@ describe("<ForgotPassword>", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when clicking »Sign in instead«", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await clickButton("Sign in instead");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call onLoginClick()", () => {
|
||||||
|
expect(onLoginClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("when entering a non-email value", () => {
|
describe("when entering a non-email value", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await typeIntoField("Email address", "not en email");
|
await typeIntoField("Email address", "not en email");
|
||||||
|
@ -132,7 +153,7 @@ describe("<ForgotPassword>", () => {
|
||||||
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
||||||
errcode: "M_THREEPID_NOT_FOUND",
|
errcode: "M_THREEPID_NOT_FOUND",
|
||||||
});
|
});
|
||||||
await submitForm("Send email");
|
await clickButton("Send email");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show an email not found message", () => {
|
it("should show an email not found message", () => {
|
||||||
|
@ -146,7 +167,7 @@ describe("<ForgotPassword>", () => {
|
||||||
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
||||||
name: "ConnectionError",
|
name: "ConnectionError",
|
||||||
});
|
});
|
||||||
await submitForm("Send email");
|
await clickButton("Send email");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show an info about that", () => {
|
it("should show an info about that", () => {
|
||||||
|
@ -166,7 +187,7 @@ describe("<ForgotPassword>", () => {
|
||||||
serverIsAlive: false,
|
serverIsAlive: false,
|
||||||
serverDeadError: "server down",
|
serverDeadError: "server down",
|
||||||
});
|
});
|
||||||
await submitForm("Send email");
|
await clickButton("Send email");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the server error", () => {
|
it("should show the server error", () => {
|
||||||
|
@ -180,7 +201,7 @@ describe("<ForgotPassword>", () => {
|
||||||
mocked(client).requestPasswordEmailToken.mockResolvedValue({
|
mocked(client).requestPasswordEmailToken.mockResolvedValue({
|
||||||
sid: testSid,
|
sid: testSid,
|
||||||
});
|
});
|
||||||
await submitForm("Send email");
|
await clickButton("Send email");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should send the mail and show the check email view", () => {
|
it("should send the mail and show the check email view", () => {
|
||||||
|
@ -193,6 +214,16 @@ describe("<ForgotPassword>", () => {
|
||||||
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when clicking re-enter email", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await clickButton("Re-enter email address");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("go back to the email input", () => {
|
||||||
|
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("when clicking resend email", () => {
|
describe("when clicking resend email", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await userEvent.click(screen.getByText("Resend"), { delay: null });
|
await userEvent.click(screen.getByText("Resend"), { delay: null });
|
||||||
|
@ -212,7 +243,7 @@ describe("<ForgotPassword>", () => {
|
||||||
|
|
||||||
describe("when clicking next", () => {
|
describe("when clicking next", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await submitForm("Next");
|
await clickButton("Next");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the password input view", () => {
|
it("should show the password input view", () => {
|
||||||
|
@ -246,7 +277,7 @@ describe("<ForgotPassword>", () => {
|
||||||
retry_after_ms: (13 * 60 + 37) * 1000,
|
retry_after_ms: (13 * 60 + 37) * 1000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await submitForm("Reset password");
|
await clickButton("Reset password");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the rate limit error message", () => {
|
it("should show the rate limit error message", () => {
|
||||||
|
@ -258,7 +289,7 @@ describe("<ForgotPassword>", () => {
|
||||||
|
|
||||||
describe("and submitting it", () => {
|
describe("and submitting it", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await submitForm("Reset password");
|
await clickButton("Reset password");
|
||||||
// double flush promises for the modal to appear
|
// double flush promises for the modal to appear
|
||||||
await flushPromisesWithFakeTimers();
|
await flushPromisesWithFakeTimers();
|
||||||
await flushPromisesWithFakeTimers();
|
await flushPromisesWithFakeTimers();
|
||||||
|
@ -284,6 +315,46 @@ describe("<ForgotPassword>", () => {
|
||||||
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("and dismissing the dialog by clicking the background", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByTestId("dialog-background"), { delay: null });
|
||||||
|
});
|
||||||
|
// double flush promises for the modal to disappear
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldCloseTheDialogAndShowThePasswordInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and dismissing the dialog", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByLabelText("Close dialog"), { delay: null });
|
||||||
|
});
|
||||||
|
// double flush promises for the modal to disappear
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldCloseTheDialogAndShowThePasswordInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking re-enter email", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await clickButton("Re-enter email address");
|
||||||
|
// double flush promises for the modal to disappear
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close the dialog and go back to the email input", () => {
|
||||||
|
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("when validating the link from the mail", () => {
|
describe("when validating the link from the mail", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mocked(client.setPassword).mockResolvedValue({});
|
mocked(client.setPassword).mockResolvedValue({});
|
||||||
|
|
|
@ -20,6 +20,7 @@ import React from "react";
|
||||||
import {
|
import {
|
||||||
StatelessNotificationBadge,
|
StatelessNotificationBadge,
|
||||||
} from "../../../../../src/components/views/rooms/NotificationBadge/StatelessNotificationBadge";
|
} from "../../../../../src/components/views/rooms/NotificationBadge/StatelessNotificationBadge";
|
||||||
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor";
|
import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor";
|
||||||
|
|
||||||
describe("NotificationBadge", () => {
|
describe("NotificationBadge", () => {
|
||||||
|
@ -45,5 +46,19 @@ describe("NotificationBadge", () => {
|
||||||
fireEvent.mouseLeave(container.firstChild);
|
fireEvent.mouseLeave(container.firstChild);
|
||||||
expect(cb).toHaveBeenCalledTimes(3);
|
expect(cb).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("hides the bold icon when the settings is set", () => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
|
||||||
|
return name === "feature_hidebold";
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = render(<StatelessNotificationBadge
|
||||||
|
symbol=""
|
||||||
|
color={NotificationColor.Bold}
|
||||||
|
count={1}
|
||||||
|
/>);
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -32,7 +32,7 @@ import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-l
|
||||||
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
|
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
|
||||||
import RoomList from "../../../../src/components/views/rooms/RoomList";
|
import RoomList from "../../../../src/components/views/rooms/RoomList";
|
||||||
import RoomSublist from "../../../../src/components/views/rooms/RoomSublist";
|
import RoomSublist from "../../../../src/components/views/rooms/RoomSublist";
|
||||||
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
|
import { RoomTile } from "../../../../src/components/views/rooms/RoomTile";
|
||||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from '../../../test-utils';
|
import { getMockClientWithEventEmitter, mockClientMethodsUser } from '../../../test-utils';
|
||||||
import ResizeNotifier from '../../../../src/utils/ResizeNotifier';
|
import ResizeNotifier from '../../../../src/utils/ResizeNotifier';
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,13 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen, act } from "@testing-library/react";
|
import { render, screen, act, RenderResult } from "@testing-library/react";
|
||||||
import { mocked, Mocked } from "jest-mock";
|
import { mocked, Mocked } from "jest-mock";
|
||||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||||
import { Widget } from "matrix-widget-api";
|
import { Widget } from "matrix-widget-api";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||||
|
@ -30,6 +31,7 @@ import {
|
||||||
MockedCall,
|
MockedCall,
|
||||||
useMockedCalls,
|
useMockedCalls,
|
||||||
setupAsyncStoreWithClient,
|
setupAsyncStoreWithClient,
|
||||||
|
filterConsole,
|
||||||
} from "../../../test-utils";
|
} from "../../../test-utils";
|
||||||
import { CallStore } from "../../../../src/stores/CallStore";
|
import { CallStore } from "../../../../src/stores/CallStore";
|
||||||
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
|
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
|
||||||
|
@ -39,30 +41,54 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import PlatformPeg from "../../../../src/PlatformPeg";
|
import PlatformPeg from "../../../../src/PlatformPeg";
|
||||||
import BasePlatform from "../../../../src/BasePlatform";
|
import BasePlatform from "../../../../src/BasePlatform";
|
||||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||||
|
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
|
||||||
|
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||||
|
|
||||||
describe("RoomTile", () => {
|
describe("RoomTile", () => {
|
||||||
jest.spyOn(PlatformPeg, "get")
|
jest.spyOn(PlatformPeg, "get")
|
||||||
.mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform);
|
.mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform);
|
||||||
useMockedCalls();
|
useMockedCalls();
|
||||||
|
|
||||||
|
const setUpVoiceBroadcast = (state: VoiceBroadcastInfoState): void => {
|
||||||
|
voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
state,
|
||||||
|
client.getUserId(),
|
||||||
|
client.getDeviceId(),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
room.currentState.setStateEvents([voiceBroadcastInfoEvent]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRoomTile = (): void => {
|
||||||
|
renderResult = render(
|
||||||
|
<RoomTile
|
||||||
|
room={room}
|
||||||
|
showMessagePreview={false}
|
||||||
|
isMinimized={false}
|
||||||
|
tag={DefaultTagID.Untagged}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
let client: Mocked<MatrixClient>;
|
let client: Mocked<MatrixClient>;
|
||||||
|
let restoreConsole: () => void;
|
||||||
|
let voiceBroadcastInfoEvent: MatrixEvent;
|
||||||
|
let room: Room;
|
||||||
|
let renderResult: RenderResult;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
restoreConsole = filterConsole(
|
||||||
|
// irrelevant for this test
|
||||||
|
"Room !1:example.org does not have an m.room.create event",
|
||||||
|
);
|
||||||
|
|
||||||
stubClient();
|
stubClient();
|
||||||
client = mocked(MatrixClientPeg.get());
|
client = mocked(MatrixClientPeg.get());
|
||||||
DMRoomMap.makeShared();
|
DMRoomMap.makeShared();
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("call subtitle", () => {
|
|
||||||
let room: Room;
|
|
||||||
let call: MockedCall;
|
|
||||||
let widget: Widget;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
});
|
});
|
||||||
|
@ -71,6 +97,23 @@ describe("RoomTile", () => {
|
||||||
client.getRooms.mockReturnValue([room]);
|
client.getRooms.mockReturnValue([room]);
|
||||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||||
|
|
||||||
|
renderRoomTile();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
restoreConsole();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the room", () => {
|
||||||
|
expect(renderResult.container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when a call starts", () => {
|
||||||
|
let call: MockedCall;
|
||||||
|
let widget: Widget;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||||
|
|
||||||
|
@ -83,18 +126,10 @@ describe("RoomTile", () => {
|
||||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||||
stop: () => {},
|
stop: () => {},
|
||||||
} as unknown as ClientWidgetApi);
|
} as unknown as ClientWidgetApi);
|
||||||
|
|
||||||
render(
|
|
||||||
<RoomTile
|
|
||||||
room={room}
|
|
||||||
showMessagePreview={false}
|
|
||||||
isMinimized={false}
|
|
||||||
tag={DefaultTagID.Untagged}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
renderResult.unmount();
|
||||||
call.destroy();
|
call.destroy();
|
||||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||||
|
@ -147,5 +182,45 @@ describe("RoomTile", () => {
|
||||||
act(() => { call.participants = new Map(); });
|
act(() => { call.participants = new Map(); });
|
||||||
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("and a live broadcast starts", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should still render the call subtitle", () => {
|
||||||
|
expect(screen.queryByText("Video")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Live")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when a live voice broadcast starts", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the »Live« subtitle", () => {
|
||||||
|
expect(screen.queryByText("Live")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and the broadcast stops", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const stopEvent = mkVoiceBroadcastInfoStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
VoiceBroadcastInfoState.Stopped,
|
||||||
|
client.getUserId(),
|
||||||
|
client.getDeviceId(),
|
||||||
|
voiceBroadcastInfoEvent,
|
||||||
|
);
|
||||||
|
act(() => {
|
||||||
|
room.currentState.setStateEvents([stopEvent]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render the »Live« subtitle", () => {
|
||||||
|
expect(screen.queryByText("Live")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`RoomTile should render the room 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="!1:example.org Unread messages."
|
||||||
|
aria-selected="false"
|
||||||
|
class="mx_AccessibleButton mx_RoomTile"
|
||||||
|
role="treeitem"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DecoratedRoomAvatar"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 20.8px; width: 32px; line-height: 32px;"
|
||||||
|
>
|
||||||
|
!
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
src="data:image/png;base64,00"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomTile_titleContainer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomTile_title mx_RoomTile_titleHasUnreadEvents"
|
||||||
|
tabindex="-1"
|
||||||
|
title="!1:example.org"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
!1:example.org
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_RoomTile_badgeContainer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_dot"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_NotificationBadge_count"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-label="Room options"
|
||||||
|
class="mx_AccessibleButton mx_RoomTile_menuButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-label="Notification options"
|
||||||
|
class="mx_AccessibleButton mx_RoomTile_notificationsButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -38,6 +38,7 @@ jest.mock('../../../../src/settings/SettingsStore', () => ({
|
||||||
setValue: jest.fn(),
|
setValue: jest.fn(),
|
||||||
getValue: jest.fn(),
|
getValue: jest.fn(),
|
||||||
monitorSetting: jest.fn(),
|
monitorSetting: jest.fn(),
|
||||||
|
watchSetting: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../../../../src/dispatcher/dispatcher', () => ({
|
jest.mock('../../../../src/dispatcher/dispatcher', () => ({
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { TestSdkContext } from "../TestSdkContext";
|
||||||
jest.mock("../../src/settings/SettingsStore", () => ({
|
jest.mock("../../src/settings/SettingsStore", () => ({
|
||||||
getValue: jest.fn(),
|
getValue: jest.fn(),
|
||||||
monitorSetting: jest.fn(),
|
monitorSetting: jest.fn(),
|
||||||
|
watchSetting: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("TypingStore", () => {
|
describe("TypingStore", () => {
|
||||||
|
|
|
@ -39,7 +39,7 @@ export const filterConsole = (...ignoreList: string[]): () => void => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
originalFunction(data);
|
originalFunction(...data);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ jest.mock('../../src/Modal', () => ({
|
||||||
jest.mock('../../src/settings/SettingsStore', () => ({
|
jest.mock('../../src/settings/SettingsStore', () => ({
|
||||||
getValue: jest.fn(),
|
getValue: jest.fn(),
|
||||||
monitorSetting: jest.fn(),
|
monitorSetting: jest.fn(),
|
||||||
|
watchSetting: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {
|
const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {
|
||||||
|
|
98
test/utils/device/snoozeBulkUnverifiedDeviceReminder-test.ts
Normal file
98
test/utils/device/snoozeBulkUnverifiedDeviceReminder-test.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isBulkUnverifiedDeviceReminderSnoozed,
|
||||||
|
snoozeBulkUnverifiedDeviceReminder,
|
||||||
|
} from "../../../src/utils/device/snoozeBulkUnverifiedDeviceReminder";
|
||||||
|
|
||||||
|
const SNOOZE_KEY = 'mx_snooze_bulk_unverified_device_nag';
|
||||||
|
|
||||||
|
describe('snooze bulk unverified device nag', () => {
|
||||||
|
const localStorageSetSpy = jest.spyOn(localStorage.__proto__, 'setItem');
|
||||||
|
const localStorageGetSpy = jest.spyOn(localStorage.__proto__, 'getItem');
|
||||||
|
const localStorageRemoveSpy = jest.spyOn(localStorage.__proto__, 'removeItem');
|
||||||
|
|
||||||
|
// 14.03.2022 16:15
|
||||||
|
const now = 1647270879403;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageSetSpy.mockClear().mockImplementation(() => {});
|
||||||
|
localStorageGetSpy.mockClear().mockReturnValue(null);
|
||||||
|
localStorageRemoveSpy.mockClear().mockImplementation(() => {});
|
||||||
|
|
||||||
|
jest.spyOn(Date, 'now').mockReturnValue(now);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('snoozeBulkUnverifiedDeviceReminder()', () => {
|
||||||
|
it('sets the current time in local storage', () => {
|
||||||
|
snoozeBulkUnverifiedDeviceReminder();
|
||||||
|
|
||||||
|
expect(localStorageSetSpy).toHaveBeenCalledWith(SNOOZE_KEY, now.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('catches an error from localstorage', () => {
|
||||||
|
const loggerErrorSpy = jest.spyOn(logger, 'error');
|
||||||
|
localStorageSetSpy.mockImplementation(() => { throw new Error('oups'); });
|
||||||
|
snoozeBulkUnverifiedDeviceReminder();
|
||||||
|
expect(loggerErrorSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isBulkUnverifiedDeviceReminderSnoozed()', () => {
|
||||||
|
it('returns false when there is no snooze in storage', () => {
|
||||||
|
const result = isBulkUnverifiedDeviceReminderSnoozed();
|
||||||
|
expect(localStorageGetSpy).toHaveBeenCalledWith(SNOOZE_KEY);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('catches an error from localstorage and returns false', () => {
|
||||||
|
const loggerErrorSpy = jest.spyOn(logger, 'error');
|
||||||
|
localStorageGetSpy.mockImplementation(() => { throw new Error('oups'); });
|
||||||
|
const result = isBulkUnverifiedDeviceReminderSnoozed();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(loggerErrorSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when snooze timestamp in storage is not a number', () => {
|
||||||
|
localStorageGetSpy.mockReturnValue('test');
|
||||||
|
const result = isBulkUnverifiedDeviceReminderSnoozed();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when snooze timestamp in storage is over a week ago', () => {
|
||||||
|
const msDay = 1000 * 60 * 60 * 24;
|
||||||
|
// snoozed 8 days ago
|
||||||
|
localStorageGetSpy.mockReturnValue(now - (msDay * 8));
|
||||||
|
const result = isBulkUnverifiedDeviceReminderSnoozed();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when snooze timestamp in storage is less than a week ago', () => {
|
||||||
|
const msDay = 1000 * 60 * 60 * 24;
|
||||||
|
// snoozed 8 days ago
|
||||||
|
localStorageGetSpy.mockReturnValue(now - (msDay * 6));
|
||||||
|
const result = isBulkUnverifiedDeviceReminderSnoozed();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
16
yarn.lock
16
yarn.lock
|
@ -2267,11 +2267,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
||||||
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
||||||
|
|
||||||
"@types/sdp-transform@^2.4.5":
|
|
||||||
version "2.4.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/sdp-transform/-/sdp-transform-2.4.5.tgz#3167961e0a1a5265545e278627aa37c606003f53"
|
|
||||||
integrity sha512-GVO0gnmbyO3Oxm2HdPsYUNcyihZE3GyCY8ysMYHuQGfLhGZq89Nm4lSzULWTzZoyHtg+VO/IdrnxZHPnPSGnAg==
|
|
||||||
|
|
||||||
"@types/semver@^7.3.12":
|
"@types/semver@^7.3.12":
|
||||||
version "7.3.13"
|
version "7.3.13"
|
||||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
|
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
|
||||||
|
@ -6357,11 +6352,10 @@ matrix-events-sdk@0.0.1:
|
||||||
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||||
version "21.2.0"
|
version "22.0.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/1606274c36008b6a976a5e4b47cdd13a1e4e5997"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/ccab6985ad5567960fa9bc4cd95fc39241560b80"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@types/sdp-transform" "^2.4.5"
|
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
bs58 "^5.0.0"
|
bs58 "^5.0.0"
|
||||||
content-type "^1.0.4"
|
content-type "^1.0.4"
|
||||||
|
@ -6372,6 +6366,7 @@ matrix-events-sdk@0.0.1:
|
||||||
qs "^6.9.6"
|
qs "^6.9.6"
|
||||||
sdp-transform "^2.14.1"
|
sdp-transform "^2.14.1"
|
||||||
unhomoglyph "^1.0.6"
|
unhomoglyph "^1.0.6"
|
||||||
|
uuid "7"
|
||||||
|
|
||||||
matrix-mock-request@^2.5.0:
|
matrix-mock-request@^2.5.0:
|
||||||
version "2.6.0"
|
version "2.6.0"
|
||||||
|
@ -8486,6 +8481,11 @@ util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||||
|
|
||||||
|
uuid@7:
|
||||||
|
version "7.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
|
||||||
|
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
|
||||||
|
|
||||||
uuid@8.3.2, uuid@^8.3.2:
|
uuid@8.3.2, uuid@^8.3.2:
|
||||||
version "8.3.2"
|
version "8.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
|
|
Loading…
Reference in a new issue