Merge remote-tracking branch 'origin/develop' into feat/emoji-picker-rich-text-mode

This commit is contained in:
Florian Duros 2022-12-06 16:38:46 +01:00
commit 1bd560d350
No known key found for this signature in database
GPG key ID: 9700AA5870258A0B
39 changed files with 805 additions and 125 deletions

View file

@ -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)
===================================================================================================== =====================================================================================================

View file

@ -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": {

View file

@ -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";

View file

@ -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 {

View file

@ -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;
}
} }
} }

View file

@ -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;
}

View file

@ -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 {

View file

@ -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>
); );

View file

@ -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.

View file

@ -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}
/>; />;

View file

@ -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")}
/>
</>; </>;
}; };

View file

@ -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>
</>; </>;

View file

@ -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")}
/>
</>; </>;
}; };

View file

@ -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);

View file

@ -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;

View file

@ -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 its you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>": "We need to know its you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>", "We need to know its you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>": "We need to know its you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>",
"Commands": "Commands", "Commands": "Commands",

View file

@ -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,

View file

@ -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;
} }

View file

@ -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;

View file

@ -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();
}; };

View file

@ -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;
} }

View file

@ -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;

View file

@ -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({

View 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;
}
};

View file

@ -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>;
};

View 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;
};

View file

@ -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";

View file

@ -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) => {

View file

@ -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({});

View file

@ -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();
});
}); });
}); });

View file

@ -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';

View file

@ -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();
});
});
}); });
}); });

View file

@ -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>
`;

View file

@ -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', () => ({

View file

@ -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", () => {

View file

@ -39,7 +39,7 @@ export const filterConsole = (...ignoreList: string[]): () => void => {
return; return;
} }
originalFunction(data); originalFunction(...data);
}; };
} }

View file

@ -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) => {

View 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);
});
});
});

View file

@ -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"