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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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