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)
|
||||
=====================================================================================================
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.61.0",
|
||||
"version": "3.62.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
|
|
@ -375,4 +375,5 @@
|
|||
@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss";
|
||||
@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss";
|
||||
|
|
|
@ -137,15 +137,50 @@ limitations under the License.
|
|||
}
|
||||
|
||||
/* specialisation for password reset views */
|
||||
.mx_AuthBody_forgot-password {
|
||||
.mx_AuthBody.mx_AuthBody_forgot-password {
|
||||
font-size: $font-14px;
|
||||
color: $primary-content;
|
||||
padding: 50px 32px;
|
||||
min-height: 600px;
|
||||
|
||||
h1 {
|
||||
margin-bottom: $spacing-20;
|
||||
margin-top: $spacing-24;
|
||||
margin: $spacing-24 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
display: flex;
|
||||
gap: $spacing-8;
|
||||
margin-bottom: 10px;
|
||||
margin-top: $spacing-24;
|
||||
}
|
||||
|
||||
.mx_AuthBody_did-not-receive--centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mx_AuthBody_resend-button {
|
||||
|
@ -168,7 +197,7 @@ limitations under the License.
|
|||
color: $accent;
|
||||
display: flex;
|
||||
gap: $spacing-4;
|
||||
padding: 4px;
|
||||
padding: $spacing-4;
|
||||
|
||||
&:hover {
|
||||
background-color: $system;
|
||||
|
@ -209,7 +238,7 @@ limitations under the License.
|
|||
text-align: center;
|
||||
|
||||
.mx_AuthBody_paddedFooter_title {
|
||||
margin-top: 16px;
|
||||
margin-top: $spacing-16;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
|
||||
|
@ -220,7 +249,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_AuthBody_paddedFooter_subtitle {
|
||||
margin-top: 8px;
|
||||
margin-top: $spacing-8;
|
||||
font-size: $font-10px;
|
||||
line-height: $font-14px;
|
||||
}
|
||||
|
@ -236,7 +265,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_SSOButtons + .mx_AuthBody_changeFlow {
|
||||
margin-top: 24px;
|
||||
margin-top: $spacing-24;
|
||||
}
|
||||
|
||||
.mx_AuthBody_spinner {
|
||||
|
|
|
@ -20,8 +20,8 @@ limitations under the License.
|
|||
|
||||
.mx_Dialog {
|
||||
color: $primary-content;
|
||||
font-size: 14px;
|
||||
padding: 16px;
|
||||
font-size: $font-14px;
|
||||
padding: $spacing-24 $spacing-24 $spacing-16;
|
||||
text-align: center;
|
||||
width: 485px;
|
||||
|
||||
|
@ -34,5 +34,14 @@ limitations under the License.
|
|||
color: $secondary-content;
|
||||
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";
|
||||
import SettingsStore, { CallbackFn } from "./settings/SettingsStore";
|
||||
import { UIFeature } from "./settings/UIFeature";
|
||||
import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder";
|
||||
|
||||
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("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(','));
|
||||
|
||||
const isBulkUnverifiedSessionsReminderSnoozed = isBulkUnverifiedDeviceReminderSnoozed();
|
||||
|
||||
// Display or hide the batch toast for old unverified sessions
|
||||
// don't show the toast if the current device is unverified
|
||||
if (
|
||||
oldUnverifiedDeviceIds.size > 0
|
||||
&& isCurrentDeviceTrusted
|
||||
&& this.enableBulkUnverifiedSessionsReminder
|
||||
&& !isBulkUnverifiedSessionsReminderSnoozed
|
||||
) {
|
||||
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
|
||||
} else {
|
||||
|
|
|
@ -347,7 +347,11 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
<div className="mx_Dialog">
|
||||
{ this.staticModal.elem }
|
||||
</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>
|
||||
);
|
||||
|
||||
|
@ -368,7 +372,11 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
<div className="mx_Dialog">
|
||||
{ modal.elem }
|
||||
</div>
|
||||
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
|
||||
<div
|
||||
data-testid="dialog-background"
|
||||
className="mx_Dialog_background"
|
||||
onClick={this.onBackgroundClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -19,8 +19,6 @@ import { createClient, IRequestTokenResponse, MatrixClient } from 'matrix-js-sdk
|
|||
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
const CHECK_EMAIL_VERIFIED_POLL_INTERVAL = 2000;
|
||||
|
||||
/**
|
||||
* Allows a user to reset their password on a homeserver.
|
||||
*
|
||||
|
@ -108,24 +106,6 @@ export default class PasswordReset {
|
|||
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
|
||||
* for the mxid linked to the email.
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import React, { ReactNode } from 'react';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from 'matrix-js-sdk/src/utils';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import Modal from "../../../Modal";
|
||||
|
@ -43,6 +44,8 @@ import Spinner from '../../views/elements/Spinner';
|
|||
import { formatSeconds } from '../../../DateUtils';
|
||||
import AutoDiscoveryUtils from '../../../utils/AutoDiscoveryUtils';
|
||||
|
||||
const emailCheckInterval = 2000;
|
||||
|
||||
enum Phase {
|
||||
// Show email input
|
||||
EnterEmail = 1,
|
||||
|
@ -60,7 +63,7 @@ enum Phase {
|
|||
|
||||
interface Props {
|
||||
serverConfig: ValidatedServerConfig;
|
||||
onLoginClick?: () => void;
|
||||
onLoginClick: () => void;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
|
@ -277,22 +280,43 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
|||
{
|
||||
email: this.state.email,
|
||||
errorText: this.state.errorText,
|
||||
onCloseClick: () => {
|
||||
modal.close();
|
||||
this.setState({ phase: Phase.PasswordInput });
|
||||
},
|
||||
onReEnterEmailClick: () => {
|
||||
modal.close();
|
||||
this.setState({ phase: Phase.EnterEmail });
|
||||
},
|
||||
onResendClick: this.sendVerificationMail,
|
||||
},
|
||||
"mx_VerifyEMailDialog",
|
||||
false,
|
||||
false,
|
||||
{
|
||||
// this modal cannot be dismissed except reset is done or forced
|
||||
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);
|
||||
this.phase = Phase.Done;
|
||||
modal.close();
|
||||
// Don't retry if the phase changed. For example when going back to email input.
|
||||
while (this.state.phase === Phase.ResettingPassword) {
|
||||
try {
|
||||
await this.reset.setNewPassword(this.state.password);
|
||||
this.setState({ phase: Phase.Done });
|
||||
modal.close();
|
||||
} catch (e) {
|
||||
// Email not confirmed, yet. Retry after a while.
|
||||
await sleep(emailCheckInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
loading={this.state.phase === Phase.SendingEmail}
|
||||
onInputChanged={this.onInputChanged}
|
||||
onLoginClick={this.props.onLoginClick!} // set by default props
|
||||
onSubmitForm={this.onSubmitForm}
|
||||
/>;
|
||||
}
|
||||
|
@ -374,6 +399,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
|||
return <CheckEmail
|
||||
email={this.state.email}
|
||||
errorText={this.state.errorText}
|
||||
onReEnterEmailClick={() => this.setState({ phase: Phase.EnterEmail })}
|
||||
onResendClick={this.sendVerificationMail}
|
||||
onSubmitForm={this.onSubmitForm}
|
||||
/>;
|
||||
|
|
|
@ -27,6 +27,7 @@ import { ErrorMessage } from "../../ErrorMessage";
|
|||
interface CheckEmailProps {
|
||||
email: string;
|
||||
errorText: string | ReactNode | null;
|
||||
onReEnterEmailClick: () => void;
|
||||
onResendClick: () => Promise<boolean>;
|
||||
onSubmitForm: (ev: React.FormEvent) => void;
|
||||
}
|
||||
|
@ -37,6 +38,7 @@ interface CheckEmailProps {
|
|||
export const CheckEmail: React.FC<CheckEmailProps> = ({
|
||||
email,
|
||||
errorText,
|
||||
onReEnterEmailClick,
|
||||
onSubmitForm,
|
||||
onResendClick,
|
||||
}) => {
|
||||
|
@ -50,13 +52,32 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
|||
return <>
|
||||
<EMailPromptIcon className="mx_AuthBody_emailPromptIcon--shifted" />
|
||||
<h1>{ _t("Check your email to continue") }</h1>
|
||||
<p>
|
||||
{ _t(
|
||||
"Follow the instructions sent to <b>%(email)s</b>",
|
||||
{ email: email },
|
||||
{ b: t => <b>{ t }</b> },
|
||||
) }
|
||||
</p>
|
||||
<div className="mx_AuthBody_text">
|
||||
<p>
|
||||
{ _t(
|
||||
"Follow the instructions sent to <b>%(email)s</b>",
|
||||
{ email: email },
|
||||
{ b: t => <b>{ t }</b> },
|
||||
) }
|
||||
</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">
|
||||
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
||||
<AccessibleButton
|
||||
|
@ -73,12 +94,5 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
|||
/>
|
||||
</AccessibleButton>
|
||||
</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 Spinner from "../../../views/elements/Spinner";
|
||||
import Field from "../../../views/elements/Field";
|
||||
import AccessibleButton from "../../../views/elements/AccessibleButton";
|
||||
|
||||
interface EnterEmailProps {
|
||||
email: string;
|
||||
|
@ -29,6 +30,7 @@ interface EnterEmailProps {
|
|||
homeserver: string;
|
||||
loading: boolean;
|
||||
onInputChanged: (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => void;
|
||||
onLoginClick: () => void;
|
||||
onSubmitForm: (ev: React.FormEvent) => void;
|
||||
}
|
||||
|
||||
|
@ -41,6 +43,7 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
|||
homeserver,
|
||||
loading,
|
||||
onInputChanged,
|
||||
onLoginClick,
|
||||
onSubmitForm,
|
||||
}) => {
|
||||
const submitButtonChild = loading
|
||||
|
@ -92,6 +95,15 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
|||
>
|
||||
{ submitButtonChild }
|
||||
</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>
|
||||
</form>
|
||||
</>;
|
||||
|
|
|
@ -27,12 +27,16 @@ import { ErrorMessage } from "../../ErrorMessage";
|
|||
interface Props {
|
||||
email: string;
|
||||
errorText: string | null;
|
||||
onCloseClick: () => void;
|
||||
onReEnterEmailClick: () => void;
|
||||
onResendClick: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const VerifyEmailModal: React.FC<Props> = ({
|
||||
email,
|
||||
errorText,
|
||||
onCloseClick,
|
||||
onReEnterEmailClick,
|
||||
onResendClick,
|
||||
}) => {
|
||||
const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500);
|
||||
|
@ -57,7 +61,8 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
|||
},
|
||||
) }
|
||||
</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>
|
||||
<AccessibleButton
|
||||
className="mx_AuthBody_resend-button"
|
||||
|
@ -74,5 +79,22 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
|||
</AccessibleButton>
|
||||
{ errorText && <ErrorMessage message={errorText} /> }
|
||||
</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 AccessibleButton from "../../elements/AccessibleButton";
|
||||
import { NotificationColor } from "../../../../stores/notifications/NotificationColor";
|
||||
import { useSettingValue } from "../../../../hooks/useSettings";
|
||||
|
||||
interface Props {
|
||||
symbol: string | null;
|
||||
|
@ -37,8 +38,12 @@ export function StatelessNotificationBadge({
|
|||
count,
|
||||
color,
|
||||
...props }: Props) {
|
||||
const hideBold = useSettingValue("feature_hidebold");
|
||||
|
||||
// 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);
|
||||
|
||||
|
|
|
@ -48,20 +48,25 @@ import { RoomTileCallSummary } from "./RoomTileCallSummary";
|
|||
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
||||
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { useHasRoomLiveVoiceBroadcast, VoiceBroadcastRoomSubtitle } from "../../../voice-broadcast";
|
||||
|
||||
interface IProps {
|
||||
interface Props {
|
||||
room: Room;
|
||||
showMessagePreview: boolean;
|
||||
isMinimized: boolean;
|
||||
tag: TagID;
|
||||
}
|
||||
|
||||
interface ClassProps extends Props {
|
||||
hasLiveVoiceBroadcast: boolean;
|
||||
}
|
||||
|
||||
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
||||
|
||||
interface IState {
|
||||
interface State {
|
||||
selected: boolean;
|
||||
notificationsMenuPosition: PartialDOMRect;
|
||||
generalMenuPosition: PartialDOMRect;
|
||||
notificationsMenuPosition: PartialDOMRect | null;
|
||||
generalMenuPosition: PartialDOMRect | null;
|
||||
call: Call | null;
|
||||
messagePreview?: string;
|
||||
}
|
||||
|
@ -76,13 +81,13 @@ export const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
|||
return { left, top, chevronFace };
|
||||
};
|
||||
|
||||
export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
export class RoomTile extends React.PureComponent<ClassProps, State> {
|
||||
private dispatcherRef?: string;
|
||||
private roomTileRef = createRef<HTMLDivElement>();
|
||||
private notificationState: NotificationState;
|
||||
private roomProps: RoomEchoChamber;
|
||||
|
||||
constructor(props: IProps) {
|
||||
constructor(props: ClassProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
@ -120,7 +125,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
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 minimizedChanged = prevProps.isMinimized !== this.props.isMinimized;
|
||||
if (showMessageChanged || minimizedChanged) {
|
||||
|
@ -169,7 +174,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.onRoomPreviewChanged,
|
||||
);
|
||||
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.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
CallStore.instance.off(CallStoreEvent.Call, this.onCallChanged);
|
||||
|
@ -218,12 +223,14 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
const clearSearch = ([KeyBindingAction.Enter, KeyBindingAction.Space] as Array<string | undefined>)
|
||||
.includes(action);
|
||||
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
show_room_tile: true, // make sure the room is visible in the list
|
||||
room_id: this.props.room.roomId,
|
||||
clear_search: [KeyBindingAction.Enter, KeyBindingAction.Space].includes(action),
|
||||
clear_search: clearSearch,
|
||||
metricsTrigger: "RoomList",
|
||||
metricsViaKeyboard: ev.type !== "click",
|
||||
});
|
||||
|
@ -233,7 +240,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.setState({ selected: isActive });
|
||||
};
|
||||
|
||||
private onNotificationsMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
private onNotificationsMenuOpenClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
|
@ -246,7 +253,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.setState({ notificationsMenuPosition: null });
|
||||
};
|
||||
|
||||
private onGeneralMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
private onGeneralMenuOpenClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
|
@ -271,7 +278,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
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 ||
|
||||
!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
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -379,6 +386,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
<RoomTileCallSummary call={this.state.call} />
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.hasLiveVoiceBroadcast) {
|
||||
subtitle = <VoiceBroadcastRoomSubtitle />;
|
||||
} else if (this.showMessagePreview && this.state.messagePreview) {
|
||||
subtitle = (
|
||||
<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 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",
|
||||
"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",
|
||||
"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)",
|
||||
|
@ -3496,6 +3497,8 @@
|
|||
"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.",
|
||||
"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?",
|
||||
"Verification link email resent!": "Verification link email resent!",
|
||||
"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.",
|
||||
"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.",
|
||||
"Sign in instead": "Sign in instead",
|
||||
"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>",
|
||||
"Commands": "Commands",
|
||||
|
|
|
@ -556,11 +556,18 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
||||
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": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
displayName: _td("Use a more compact 'Modern' layout"),
|
||||
default: false,
|
||||
controller: new IncompatibleController("layout", false, v => v !== Layout.Group),
|
||||
controller: new IncompatibleController("layout", false, (v: Layout) => v !== Layout.Group),
|
||||
},
|
||||
"showRedactions": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
||||
|
|
|
@ -31,7 +31,7 @@ export class ListNotificationState extends NotificationState {
|
|||
super();
|
||||
}
|
||||
|
||||
public get symbol(): string {
|
||||
public get symbol(): string | 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 { IDestroyable } from "../../utils/IDestroyable";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
export interface INotificationStateSnapshotParams {
|
||||
symbol: string | null;
|
||||
|
@ -37,11 +38,22 @@ export abstract class NotificationState
|
|||
extends TypedEventEmitter<NotificationStateEvents, EventHandlerMap>
|
||||
implements INotificationStateSnapshotParams, IDestroyable {
|
||||
//
|
||||
protected _symbol: string | null;
|
||||
protected _count: number;
|
||||
protected _color: NotificationColor;
|
||||
protected _symbol: string | null = null;
|
||||
protected _count = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -58,7 +70,12 @@ export abstract class NotificationState
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -81,11 +98,15 @@ export abstract class NotificationState
|
|||
|
||||
public destroy(): void {
|
||||
this.removeAllListeners(NotificationStateEvents.Update);
|
||||
for (const watcherReference of this.watcherReferences) {
|
||||
SettingsStore.unwatchSetting(watcherReference);
|
||||
}
|
||||
this.watcherReferences = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationStateSnapshot {
|
||||
private readonly symbol: string;
|
||||
private readonly symbol: string | null;
|
||||
private readonly count: number;
|
||||
private readonly color: NotificationColor;
|
||||
|
||||
|
|
|
@ -98,8 +98,8 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
|||
this.updateNotificationState();
|
||||
};
|
||||
|
||||
private handleRoomEventUpdate = (event: MatrixEvent, room: Room | null) => {
|
||||
if (room?.roomId !== this.room.roomId) return; // ignore - not for us or notifications timeline
|
||||
private handleRoomEventUpdate = (event: MatrixEvent) => {
|
||||
if (event?.getRoomId() !== this.room.roomId) return; // ignore - not for us or notifications timeline
|
||||
this.updateNotificationState();
|
||||
};
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ export class SpaceNotificationState extends NotificationState {
|
|||
super();
|
||||
}
|
||||
|
||||
public get symbol(): string {
|
||||
public get symbol(): string | null {
|
||||
return this._color === NotificationColor.Unsent ? "!" : null;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { NotificationState } from "./NotificationState";
|
|||
export class StaticNotificationState extends NotificationState {
|
||||
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();
|
||||
this._symbol = symbol;
|
||||
this._count = count;
|
||||
|
|
|
@ -20,6 +20,7 @@ import DeviceListener from '../DeviceListener';
|
|||
import GenericToast from "../components/views/toasts/GenericToast";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { snoozeBulkUnverifiedDeviceReminder } from '../utils/device/snoozeBulkUnverifiedDeviceReminder';
|
||||
|
||||
const TOAST_KEY = "reviewsessions";
|
||||
|
||||
|
@ -34,6 +35,7 @@ export const showToast = (deviceIds: Set<string>) => {
|
|||
|
||||
const onReject = () => {
|
||||
DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds);
|
||||
snoozeBulkUnverifiedDeviceReminder();
|
||||
};
|
||||
|
||||
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/VoiceBroadcastControl";
|
||||
export * from "./components/atoms/VoiceBroadcastHeader";
|
||||
export * from "./components/atoms/VoiceBroadcastRoomSubtitle";
|
||||
export * from "./components/molecules/VoiceBroadcastPlaybackBody";
|
||||
export * from "./components/molecules/VoiceBroadcastPreRecordingPip";
|
||||
export * from "./components/molecules/VoiceBroadcastRecordingBody";
|
||||
export * from "./components/molecules/VoiceBroadcastRecordingPip";
|
||||
export * from "./hooks/useCurrentVoiceBroadcastPreRecording";
|
||||
export * from "./hooks/useCurrentVoiceBroadcastRecording";
|
||||
export * from "./hooks/useHasRoomLiveVoiceBroadcast";
|
||||
export * from "./hooks/useVoiceBroadcastRecording";
|
||||
export * from "./stores/VoiceBroadcastPlaybacksStore";
|
||||
export * from "./stores/VoiceBroadcastPreRecordingStore";
|
||||
|
|
|
@ -35,6 +35,7 @@ import SettingsStore from "../src/settings/SettingsStore";
|
|||
import { SettingLevel } from "../src/settings/SettingLevel";
|
||||
import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils";
|
||||
import { UIFeature } from "../src/settings/UIFeature";
|
||||
import { isBulkUnverifiedDeviceReminderSnoozed } from "../src/utils/device/snoozeBulkUnverifiedDeviceReminder";
|
||||
|
||||
// don't litter test console with logs
|
||||
jest.mock("matrix-js-sdk/src/logger");
|
||||
|
@ -48,6 +49,10 @@ jest.mock("../src/SecurityManager", () => ({
|
|||
isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../src/utils/device/snoozeBulkUnverifiedDeviceReminder", () => ({
|
||||
isBulkUnverifiedDeviceReminderSnoozed: jest.fn(),
|
||||
}));
|
||||
|
||||
const userId = '@user:server';
|
||||
const deviceId = 'my-device-id';
|
||||
const mockDispatcher = mocked(dis);
|
||||
|
@ -95,6 +100,7 @@ describe('DeviceListener', () => {
|
|||
});
|
||||
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient);
|
||||
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
|
||||
mocked(isBulkUnverifiedDeviceReminderSnoozed).mockClear().mockReturnValue(false);
|
||||
});
|
||||
|
||||
const createAndStart = async (): Promise<DeviceListener> => {
|
||||
|
@ -451,6 +457,23 @@ describe('DeviceListener', () => {
|
|||
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 () => {
|
||||
// currentDevice, device2 are verified, device3 is unverified
|
||||
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {
|
||||
|
|
|
@ -38,6 +38,7 @@ describe("<ForgotPassword>", () => {
|
|||
let client: MatrixClient;
|
||||
let serverConfig: ValidatedServerConfig;
|
||||
let onComplete: () => void;
|
||||
let onLoginClick: () => void;
|
||||
let renderResult: RenderResult;
|
||||
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 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";
|
||||
|
||||
onComplete = jest.fn();
|
||||
onLoginClick = jest.fn();
|
||||
|
||||
jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig);
|
||||
jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError");
|
||||
|
@ -94,6 +103,7 @@ describe("<ForgotPassword>", () => {
|
|||
renderResult = render(<ForgotPassword
|
||||
serverConfig={serverConfig}
|
||||
onComplete={onComplete}
|
||||
onLoginClick={onLoginClick}
|
||||
/>);
|
||||
});
|
||||
|
||||
|
@ -108,6 +118,7 @@ describe("<ForgotPassword>", () => {
|
|||
renderResult.rerender(<ForgotPassword
|
||||
serverConfig={serverConfig}
|
||||
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", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("Email address", "not en email");
|
||||
|
@ -132,7 +153,7 @@ describe("<ForgotPassword>", () => {
|
|||
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
||||
errcode: "M_THREEPID_NOT_FOUND",
|
||||
});
|
||||
await submitForm("Send email");
|
||||
await clickButton("Send email");
|
||||
});
|
||||
|
||||
it("should show an email not found message", () => {
|
||||
|
@ -146,7 +167,7 @@ describe("<ForgotPassword>", () => {
|
|||
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
||||
name: "ConnectionError",
|
||||
});
|
||||
await submitForm("Send email");
|
||||
await clickButton("Send email");
|
||||
});
|
||||
|
||||
it("should show an info about that", () => {
|
||||
|
@ -166,7 +187,7 @@ describe("<ForgotPassword>", () => {
|
|||
serverIsAlive: false,
|
||||
serverDeadError: "server down",
|
||||
});
|
||||
await submitForm("Send email");
|
||||
await clickButton("Send email");
|
||||
});
|
||||
|
||||
it("should show the server error", () => {
|
||||
|
@ -180,7 +201,7 @@ describe("<ForgotPassword>", () => {
|
|||
mocked(client).requestPasswordEmailToken.mockResolvedValue({
|
||||
sid: testSid,
|
||||
});
|
||||
await submitForm("Send email");
|
||||
await clickButton("Send email");
|
||||
});
|
||||
|
||||
it("should send the mail and show the check email view", () => {
|
||||
|
@ -193,6 +214,16 @@ describe("<ForgotPassword>", () => {
|
|||
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", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText("Resend"), { delay: null });
|
||||
|
@ -212,7 +243,7 @@ describe("<ForgotPassword>", () => {
|
|||
|
||||
describe("when clicking next", () => {
|
||||
beforeEach(async () => {
|
||||
await submitForm("Next");
|
||||
await clickButton("Next");
|
||||
});
|
||||
|
||||
it("should show the password input view", () => {
|
||||
|
@ -246,7 +277,7 @@ describe("<ForgotPassword>", () => {
|
|||
retry_after_ms: (13 * 60 + 37) * 1000,
|
||||
},
|
||||
});
|
||||
await submitForm("Reset password");
|
||||
await clickButton("Reset password");
|
||||
});
|
||||
|
||||
it("should show the rate limit error message", () => {
|
||||
|
@ -258,7 +289,7 @@ describe("<ForgotPassword>", () => {
|
|||
|
||||
describe("and submitting it", () => {
|
||||
beforeEach(async () => {
|
||||
await submitForm("Reset password");
|
||||
await clickButton("Reset password");
|
||||
// double flush promises for the modal to appear
|
||||
await flushPromisesWithFakeTimers();
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
@ -284,6 +315,46 @@ describe("<ForgotPassword>", () => {
|
|||
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", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.setPassword).mockResolvedValue({});
|
||||
|
|
|
@ -20,6 +20,7 @@ import React from "react";
|
|||
import {
|
||||
StatelessNotificationBadge,
|
||||
} from "../../../../../src/components/views/rooms/NotificationBadge/StatelessNotificationBadge";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor";
|
||||
|
||||
describe("NotificationBadge", () => {
|
||||
|
@ -45,5 +46,19 @@ describe("NotificationBadge", () => {
|
|||
fireEvent.mouseLeave(container.firstChild);
|
||||
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 RoomList from "../../../../src/components/views/rooms/RoomList";
|
||||
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 ResizeNotifier from '../../../../src/utils/ResizeNotifier';
|
||||
|
||||
|
|
|
@ -15,12 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
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 { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
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 { ClientWidgetApi } from "matrix-widget-api";
|
||||
|
@ -30,6 +31,7 @@ import {
|
|||
MockedCall,
|
||||
useMockedCalls,
|
||||
setupAsyncStoreWithClient,
|
||||
filterConsole,
|
||||
} from "../../../test-utils";
|
||||
import { CallStore } from "../../../../src/stores/CallStore";
|
||||
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
|
||||
|
@ -39,38 +41,79 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
|||
import PlatformPeg from "../../../../src/PlatformPeg";
|
||||
import BasePlatform from "../../../../src/BasePlatform";
|
||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||
|
||||
describe("RoomTile", () => {
|
||||
jest.spyOn(PlatformPeg, "get")
|
||||
.mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform);
|
||||
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 restoreConsole: () => void;
|
||||
let voiceBroadcastInfoEvent: MatrixEvent;
|
||||
let room: Room;
|
||||
let renderResult: RenderResult;
|
||||
|
||||
beforeEach(() => {
|
||||
restoreConsole = filterConsole(
|
||||
// irrelevant for this test
|
||||
"Room !1:example.org does not have an m.room.create event",
|
||||
);
|
||||
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.get());
|
||||
DMRoomMap.makeShared();
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
renderRoomTile();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
restoreConsole();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("call subtitle", () => {
|
||||
let room: Room;
|
||||
it("should render the room", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when a call starts", () => {
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
|
@ -83,18 +126,10 @@ describe("RoomTile", () => {
|
|||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
|
||||
render(
|
||||
<RoomTile
|
||||
room={room}
|
||||
showMessagePreview={false}
|
||||
isMinimized={false}
|
||||
tag={DefaultTagID.Untagged}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
renderResult.unmount();
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
|
@ -147,5 +182,45 @@ describe("RoomTile", () => {
|
|||
act(() => { call.participants = new Map(); });
|
||||
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(),
|
||||
getValue: jest.fn(),
|
||||
monitorSetting: jest.fn(),
|
||||
watchSetting: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../src/dispatcher/dispatcher', () => ({
|
||||
|
|
|
@ -25,6 +25,7 @@ import { TestSdkContext } from "../TestSdkContext";
|
|||
jest.mock("../../src/settings/SettingsStore", () => ({
|
||||
getValue: jest.fn(),
|
||||
monitorSetting: jest.fn(),
|
||||
watchSetting: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("TypingStore", () => {
|
||||
|
|
|
@ -39,7 +39,7 @@ export const filterConsole = (...ignoreList: string[]): () => void => {
|
|||
return;
|
||||
}
|
||||
|
||||
originalFunction(data);
|
||||
originalFunction(...data);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ jest.mock('../../src/Modal', () => ({
|
|||
jest.mock('../../src/settings/SettingsStore', () => ({
|
||||
getValue: jest.fn(),
|
||||
monitorSetting: jest.fn(),
|
||||
watchSetting: jest.fn(),
|
||||
}));
|
||||
|
||||
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"
|
||||
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":
|
||||
version "7.3.13"
|
||||
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==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||
version "21.2.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/1606274c36008b6a976a5e4b47cdd13a1e4e5997"
|
||||
version "22.0.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/ccab6985ad5567960fa9bc4cd95fc39241560b80"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/sdp-transform" "^2.4.5"
|
||||
another-json "^0.2.0"
|
||||
bs58 "^5.0.0"
|
||||
content-type "^1.0.4"
|
||||
|
@ -6372,6 +6366,7 @@ matrix-events-sdk@0.0.1:
|
|||
qs "^6.9.6"
|
||||
sdp-transform "^2.14.1"
|
||||
unhomoglyph "^1.0.6"
|
||||
uuid "7"
|
||||
|
||||
matrix-mock-request@^2.5.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"
|
||||
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:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
|
|
Loading…
Reference in a new issue