Merge remote-tracking branch 'origin/develop' into feat/emoji-picker-rich-text-mode
This commit is contained in:
commit
1bd560d350
39 changed files with 805 additions and 125 deletions
|
@ -48,6 +48,7 @@ import {
|
|||
} from "./utils/device/clientInformation";
|
||||
import SettingsStore, { CallbackFn } from "./settings/SettingsStore";
|
||||
import { UIFeature } from "./settings/UIFeature";
|
||||
import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
|
@ -335,12 +336,15 @@ export default class DeviceListener {
|
|||
logger.debug("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(','));
|
||||
logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(','));
|
||||
|
||||
const isBulkUnverifiedSessionsReminderSnoozed = isBulkUnverifiedDeviceReminderSnoozed();
|
||||
|
||||
// Display or hide the batch toast for old unverified sessions
|
||||
// don't show the toast if the current device is unverified
|
||||
if (
|
||||
oldUnverifiedDeviceIds.size > 0
|
||||
&& isCurrentDeviceTrusted
|
||||
&& this.enableBulkUnverifiedSessionsReminder
|
||||
&& !isBulkUnverifiedSessionsReminderSnoozed
|
||||
) {
|
||||
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
|
||||
} else {
|
||||
|
|
|
@ -347,7 +347,11 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
<div className="mx_Dialog">
|
||||
{ this.staticModal.elem }
|
||||
</div>
|
||||
<div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.onBackgroundClick} />
|
||||
<div
|
||||
data-testid="dialog-background"
|
||||
className="mx_Dialog_background mx_Dialog_staticBackground"
|
||||
onClick={this.onBackgroundClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -368,7 +372,11 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
<div className="mx_Dialog">
|
||||
{ modal.elem }
|
||||
</div>
|
||||
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
|
||||
<div
|
||||
data-testid="dialog-background"
|
||||
className="mx_Dialog_background"
|
||||
onClick={this.onBackgroundClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -19,8 +19,6 @@ import { createClient, IRequestTokenResponse, MatrixClient } from 'matrix-js-sdk
|
|||
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
const CHECK_EMAIL_VERIFIED_POLL_INTERVAL = 2000;
|
||||
|
||||
/**
|
||||
* Allows a user to reset their password on a homeserver.
|
||||
*
|
||||
|
@ -108,24 +106,6 @@ export default class PasswordReset {
|
|||
await this.checkEmailLinkClicked();
|
||||
}
|
||||
|
||||
public async retrySetNewPassword(password: string): Promise<void> {
|
||||
this.password = password;
|
||||
return new Promise((resolve) => {
|
||||
this.tryCheckEmailLinkClicked(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
private tryCheckEmailLinkClicked(resolve: Function): void {
|
||||
this.checkEmailLinkClicked()
|
||||
.then(() => resolve())
|
||||
.catch(() => {
|
||||
window.setTimeout(
|
||||
() => this.tryCheckEmailLinkClicked(resolve),
|
||||
CHECK_EMAIL_VERIFIED_POLL_INTERVAL,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the email link has been clicked by attempting to change the password
|
||||
* for the mxid linked to the email.
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import React, { ReactNode } from 'react';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from 'matrix-js-sdk/src/utils';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import Modal from "../../../Modal";
|
||||
|
@ -43,6 +44,8 @@ import Spinner from '../../views/elements/Spinner';
|
|||
import { formatSeconds } from '../../../DateUtils';
|
||||
import AutoDiscoveryUtils from '../../../utils/AutoDiscoveryUtils';
|
||||
|
||||
const emailCheckInterval = 2000;
|
||||
|
||||
enum Phase {
|
||||
// Show email input
|
||||
EnterEmail = 1,
|
||||
|
@ -60,7 +63,7 @@ enum Phase {
|
|||
|
||||
interface Props {
|
||||
serverConfig: ValidatedServerConfig;
|
||||
onLoginClick?: () => void;
|
||||
onLoginClick: () => void;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
|
@ -277,22 +280,43 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
|||
{
|
||||
email: this.state.email,
|
||||
errorText: this.state.errorText,
|
||||
onCloseClick: () => {
|
||||
modal.close();
|
||||
this.setState({ phase: Phase.PasswordInput });
|
||||
},
|
||||
onReEnterEmailClick: () => {
|
||||
modal.close();
|
||||
this.setState({ phase: Phase.EnterEmail });
|
||||
},
|
||||
onResendClick: this.sendVerificationMail,
|
||||
},
|
||||
"mx_VerifyEMailDialog",
|
||||
false,
|
||||
false,
|
||||
{
|
||||
// this modal cannot be dismissed except reset is done or forced
|
||||
onBeforeClose: async (reason?: string) => {
|
||||
return this.state.phase === Phase.Done || reason === "force";
|
||||
if (reason === "backgroundClick") {
|
||||
// Modal dismissed by clicking the background.
|
||||
// Go one phase back.
|
||||
this.setState({ phase: Phase.PasswordInput });
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await this.reset.retrySetNewPassword(this.state.password);
|
||||
this.phase = Phase.Done;
|
||||
modal.close();
|
||||
// Don't retry if the phase changed. For example when going back to email input.
|
||||
while (this.state.phase === Phase.ResettingPassword) {
|
||||
try {
|
||||
await this.reset.setNewPassword(this.state.password);
|
||||
this.setState({ phase: Phase.Done });
|
||||
modal.close();
|
||||
} catch (e) {
|
||||
// Email not confirmed, yet. Retry after a while.
|
||||
await sleep(emailCheckInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onSubmitForm = async (ev: React.FormEvent): Promise<void> => {
|
||||
|
@ -339,6 +363,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
|||
homeserver={this.props.serverConfig.hsName}
|
||||
loading={this.state.phase === Phase.SendingEmail}
|
||||
onInputChanged={this.onInputChanged}
|
||||
onLoginClick={this.props.onLoginClick!} // set by default props
|
||||
onSubmitForm={this.onSubmitForm}
|
||||
/>;
|
||||
}
|
||||
|
@ -374,6 +399,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
|||
return <CheckEmail
|
||||
email={this.state.email}
|
||||
errorText={this.state.errorText}
|
||||
onReEnterEmailClick={() => this.setState({ phase: Phase.EnterEmail })}
|
||||
onResendClick={this.sendVerificationMail}
|
||||
onSubmitForm={this.onSubmitForm}
|
||||
/>;
|
||||
|
|
|
@ -27,6 +27,7 @@ import { ErrorMessage } from "../../ErrorMessage";
|
|||
interface CheckEmailProps {
|
||||
email: string;
|
||||
errorText: string | ReactNode | null;
|
||||
onReEnterEmailClick: () => void;
|
||||
onResendClick: () => Promise<boolean>;
|
||||
onSubmitForm: (ev: React.FormEvent) => void;
|
||||
}
|
||||
|
@ -37,6 +38,7 @@ interface CheckEmailProps {
|
|||
export const CheckEmail: React.FC<CheckEmailProps> = ({
|
||||
email,
|
||||
errorText,
|
||||
onReEnterEmailClick,
|
||||
onSubmitForm,
|
||||
onResendClick,
|
||||
}) => {
|
||||
|
@ -50,13 +52,32 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
|||
return <>
|
||||
<EMailPromptIcon className="mx_AuthBody_emailPromptIcon--shifted" />
|
||||
<h1>{ _t("Check your email to continue") }</h1>
|
||||
<p>
|
||||
{ _t(
|
||||
"Follow the instructions sent to <b>%(email)s</b>",
|
||||
{ email: email },
|
||||
{ b: t => <b>{ t }</b> },
|
||||
) }
|
||||
</p>
|
||||
<div className="mx_AuthBody_text">
|
||||
<p>
|
||||
{ _t(
|
||||
"Follow the instructions sent to <b>%(email)s</b>",
|
||||
{ email: email },
|
||||
{ b: t => <b>{ t }</b> },
|
||||
) }
|
||||
</p>
|
||||
<div className="mx_AuthBody_did-not-receive">
|
||||
<span className="mx_VerifyEMailDialog_text-light">{ _t("Wrong email address?") }</span>
|
||||
<AccessibleButton
|
||||
className="mx_AuthBody_resend-button"
|
||||
kind="link"
|
||||
onClick={onReEnterEmailClick}
|
||||
>
|
||||
{ _t("Re-enter email address") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
{ errorText && <ErrorMessage message={errorText} /> }
|
||||
<input
|
||||
onClick={onSubmitForm}
|
||||
type="button"
|
||||
className="mx_Login_submit"
|
||||
value={_t("Next")}
|
||||
/>
|
||||
<div className="mx_AuthBody_did-not-receive">
|
||||
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
||||
<AccessibleButton
|
||||
|
@ -73,12 +94,5 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
|
|||
/>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{ errorText && <ErrorMessage message={errorText} /> }
|
||||
<input
|
||||
onClick={onSubmitForm}
|
||||
type="button"
|
||||
className="mx_Login_submit"
|
||||
value={_t("Next")}
|
||||
/>
|
||||
</>;
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ import EmailField from "../../../views/auth/EmailField";
|
|||
import { ErrorMessage } from "../../ErrorMessage";
|
||||
import Spinner from "../../../views/elements/Spinner";
|
||||
import Field from "../../../views/elements/Field";
|
||||
import AccessibleButton from "../../../views/elements/AccessibleButton";
|
||||
|
||||
interface EnterEmailProps {
|
||||
email: string;
|
||||
|
@ -29,6 +30,7 @@ interface EnterEmailProps {
|
|||
homeserver: string;
|
||||
loading: boolean;
|
||||
onInputChanged: (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => void;
|
||||
onLoginClick: () => void;
|
||||
onSubmitForm: (ev: React.FormEvent) => void;
|
||||
}
|
||||
|
||||
|
@ -41,6 +43,7 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
|||
homeserver,
|
||||
loading,
|
||||
onInputChanged,
|
||||
onLoginClick,
|
||||
onSubmitForm,
|
||||
}) => {
|
||||
const submitButtonChild = loading
|
||||
|
@ -92,6 +95,15 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
|
|||
>
|
||||
{ submitButtonChild }
|
||||
</button>
|
||||
<div className="mx_AuthBody_button-container">
|
||||
<AccessibleButton
|
||||
className="mx_AuthBody_sign-in-instead-button"
|
||||
element="button"
|
||||
kind="link"
|
||||
onClick={onLoginClick}>
|
||||
{ _t("Sign in instead") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</>;
|
||||
|
|
|
@ -27,12 +27,16 @@ import { ErrorMessage } from "../../ErrorMessage";
|
|||
interface Props {
|
||||
email: string;
|
||||
errorText: string | null;
|
||||
onCloseClick: () => void;
|
||||
onReEnterEmailClick: () => void;
|
||||
onResendClick: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const VerifyEmailModal: React.FC<Props> = ({
|
||||
email,
|
||||
errorText,
|
||||
onCloseClick,
|
||||
onReEnterEmailClick,
|
||||
onResendClick,
|
||||
}) => {
|
||||
const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500);
|
||||
|
@ -57,7 +61,8 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
|||
},
|
||||
) }
|
||||
</p>
|
||||
<div className="mx_AuthBody_did-not-receive mx_AuthBody_did-not-receive--centered">
|
||||
|
||||
<div className="mx_AuthBody_did-not-receive">
|
||||
<span className="mx_VerifyEMailDialog_text-light">{ _t("Did not receive it?") }</span>
|
||||
<AccessibleButton
|
||||
className="mx_AuthBody_resend-button"
|
||||
|
@ -74,5 +79,22 @@ export const VerifyEmailModal: React.FC<Props> = ({
|
|||
</AccessibleButton>
|
||||
{ errorText && <ErrorMessage message={errorText} /> }
|
||||
</div>
|
||||
|
||||
<div className="mx_AuthBody_did-not-receive">
|
||||
<span className="mx_VerifyEMailDialog_text-light">{ _t("Wrong email address?") }</span>
|
||||
<AccessibleButton
|
||||
className="mx_AuthBody_resend-button"
|
||||
kind="link"
|
||||
onClick={onReEnterEmailClick}
|
||||
>
|
||||
{ _t("Re-enter email address") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<AccessibleButton
|
||||
onClick={onCloseClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
aria-label={_t("Close dialog")}
|
||||
/>
|
||||
</>;
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ import classNames from "classnames";
|
|||
import { formatCount } from "../../../../utils/FormattingUtils";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import { NotificationColor } from "../../../../stores/notifications/NotificationColor";
|
||||
import { useSettingValue } from "../../../../hooks/useSettings";
|
||||
|
||||
interface Props {
|
||||
symbol: string | null;
|
||||
|
@ -37,8 +38,12 @@ export function StatelessNotificationBadge({
|
|||
count,
|
||||
color,
|
||||
...props }: Props) {
|
||||
const hideBold = useSettingValue("feature_hidebold");
|
||||
|
||||
// Don't show a badge if we don't need to
|
||||
if (color === NotificationColor.None) return null;
|
||||
if (color === NotificationColor.None || (hideBold && color == NotificationColor.Bold)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol);
|
||||
|
||||
|
|
|
@ -48,20 +48,25 @@ import { RoomTileCallSummary } from "./RoomTileCallSummary";
|
|||
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
||||
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { useHasRoomLiveVoiceBroadcast, VoiceBroadcastRoomSubtitle } from "../../../voice-broadcast";
|
||||
|
||||
interface IProps {
|
||||
interface Props {
|
||||
room: Room;
|
||||
showMessagePreview: boolean;
|
||||
isMinimized: boolean;
|
||||
tag: TagID;
|
||||
}
|
||||
|
||||
interface ClassProps extends Props {
|
||||
hasLiveVoiceBroadcast: boolean;
|
||||
}
|
||||
|
||||
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
||||
|
||||
interface IState {
|
||||
interface State {
|
||||
selected: boolean;
|
||||
notificationsMenuPosition: PartialDOMRect;
|
||||
generalMenuPosition: PartialDOMRect;
|
||||
notificationsMenuPosition: PartialDOMRect | null;
|
||||
generalMenuPosition: PartialDOMRect | null;
|
||||
call: Call | null;
|
||||
messagePreview?: string;
|
||||
}
|
||||
|
@ -76,13 +81,13 @@ export const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
|||
return { left, top, chevronFace };
|
||||
};
|
||||
|
||||
export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
export class RoomTile extends React.PureComponent<ClassProps, State> {
|
||||
private dispatcherRef?: string;
|
||||
private roomTileRef = createRef<HTMLDivElement>();
|
||||
private notificationState: NotificationState;
|
||||
private roomProps: RoomEchoChamber;
|
||||
|
||||
constructor(props: IProps) {
|
||||
constructor(props: ClassProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
@ -120,7 +125,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
return !this.props.isMinimized && this.props.showMessagePreview;
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
|
||||
public componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
||||
const showMessageChanged = prevProps.showMessagePreview !== this.props.showMessagePreview;
|
||||
const minimizedChanged = prevProps.isMinimized !== this.props.isMinimized;
|
||||
if (showMessageChanged || minimizedChanged) {
|
||||
|
@ -169,7 +174,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.onRoomPreviewChanged,
|
||||
);
|
||||
this.props.room.off(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
CallStore.instance.off(CallStoreEvent.Call, this.onCallChanged);
|
||||
|
@ -218,12 +223,14 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
const clearSearch = ([KeyBindingAction.Enter, KeyBindingAction.Space] as Array<string | undefined>)
|
||||
.includes(action);
|
||||
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
show_room_tile: true, // make sure the room is visible in the list
|
||||
room_id: this.props.room.roomId,
|
||||
clear_search: [KeyBindingAction.Enter, KeyBindingAction.Space].includes(action),
|
||||
clear_search: clearSearch,
|
||||
metricsTrigger: "RoomList",
|
||||
metricsViaKeyboard: ev.type !== "click",
|
||||
});
|
||||
|
@ -233,7 +240,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.setState({ selected: isActive });
|
||||
};
|
||||
|
||||
private onNotificationsMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
private onNotificationsMenuOpenClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
|
@ -246,7 +253,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.setState({ notificationsMenuPosition: null });
|
||||
};
|
||||
|
||||
private onGeneralMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
private onGeneralMenuOpenClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
|
@ -271,7 +278,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.setState({ generalMenuPosition: null });
|
||||
};
|
||||
|
||||
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
|
||||
private renderNotificationsMenu(isActive: boolean): React.ReactElement | null {
|
||||
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
|
||||
!this.showContextMenu || this.props.isMinimized
|
||||
) {
|
||||
|
@ -313,7 +320,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
private renderGeneralMenu(): React.ReactElement {
|
||||
private renderGeneralMenu(): React.ReactElement | null {
|
||||
if (!this.showContextMenu) return null; // no menu to show
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -379,6 +386,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
<RoomTileCallSummary call={this.state.call} />
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.hasLiveVoiceBroadcast) {
|
||||
subtitle = <VoiceBroadcastRoomSubtitle />;
|
||||
} else if (this.showMessagePreview && this.state.messagePreview) {
|
||||
subtitle = (
|
||||
<div
|
||||
|
@ -472,3 +481,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RoomTileHOC: React.FC<Props> = (props: Props) => {
|
||||
const hasLiveVoiceBroadcast = useHasRoomLiveVoiceBroadcast(props.room);
|
||||
return <RoomTile {...props} hasLiveVoiceBroadcast={hasLiveVoiceBroadcast} />;
|
||||
};
|
||||
|
||||
export default RoomTileHOC;
|
||||
|
|
|
@ -960,6 +960,7 @@
|
|||
"Show stickers button": "Show stickers button",
|
||||
"Show polls button": "Show polls button",
|
||||
"Insert a trailing colon after user mentions at the start of a message": "Insert a trailing colon after user mentions at the start of a message",
|
||||
"Hide notification dot (only display counters badges)": "Hide notification dot (only display counters badges)",
|
||||
"Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout",
|
||||
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
||||
"Show join/leave messages (invites/removes/bans unaffected)": "Show join/leave messages (invites/removes/bans unaffected)",
|
||||
|
@ -3496,6 +3497,8 @@
|
|||
"Clear personal data": "Clear personal data",
|
||||
"Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.",
|
||||
"Follow the instructions sent to <b>%(email)s</b>": "Follow the instructions sent to <b>%(email)s</b>",
|
||||
"Wrong email address?": "Wrong email address?",
|
||||
"Re-enter email address": "Re-enter email address",
|
||||
"Did not receive it?": "Did not receive it?",
|
||||
"Verification link email resent!": "Verification link email resent!",
|
||||
"Send email": "Send email",
|
||||
|
@ -3503,6 +3506,7 @@
|
|||
"<b>%(homeserver)s</b> will send you a verification link to let you reset your password.": "<b>%(homeserver)s</b> will send you a verification link to let you reset your password.",
|
||||
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
|
||||
"The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.",
|
||||
"Sign in instead": "Sign in instead",
|
||||
"Verify your email to continue": "Verify your email to continue",
|
||||
"We need to know it’s you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>": "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to <b>%(email)s</b>",
|
||||
"Commands": "Commands",
|
||||
|
|
|
@ -556,11 +556,18 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
||||
default: false,
|
||||
},
|
||||
"feature_hidebold": {
|
||||
isFeature: true,
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
displayName: _td("Hide notification dot (only display counters badges)"),
|
||||
labsGroup: LabGroup.Rooms,
|
||||
default: false,
|
||||
},
|
||||
"useCompactLayout": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
displayName: _td("Use a more compact 'Modern' layout"),
|
||||
default: false,
|
||||
controller: new IncompatibleController("layout", false, v => v !== Layout.Group),
|
||||
controller: new IncompatibleController("layout", false, (v: Layout) => v !== Layout.Group),
|
||||
},
|
||||
"showRedactions": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
||||
|
|
|
@ -31,7 +31,7 @@ export class ListNotificationState extends NotificationState {
|
|||
super();
|
||||
}
|
||||
|
||||
public get symbol(): string {
|
||||
public get symbol(): string | null {
|
||||
return this._color === NotificationColor.Unsent ? "!" : null;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter"
|
|||
|
||||
import { NotificationColor } from "./NotificationColor";
|
||||
import { IDestroyable } from "../../utils/IDestroyable";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
export interface INotificationStateSnapshotParams {
|
||||
symbol: string | null;
|
||||
|
@ -37,11 +38,22 @@ export abstract class NotificationState
|
|||
extends TypedEventEmitter<NotificationStateEvents, EventHandlerMap>
|
||||
implements INotificationStateSnapshotParams, IDestroyable {
|
||||
//
|
||||
protected _symbol: string | null;
|
||||
protected _count: number;
|
||||
protected _color: NotificationColor;
|
||||
protected _symbol: string | null = null;
|
||||
protected _count = 0;
|
||||
protected _color: NotificationColor = NotificationColor.None;
|
||||
|
||||
public get symbol(): string {
|
||||
private watcherReferences: string[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.watcherReferences.push(
|
||||
SettingsStore.watchSetting("feature_hidebold", null, () => {
|
||||
this.emit(NotificationStateEvents.Update);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public get symbol(): string | null {
|
||||
return this._symbol;
|
||||
}
|
||||
|
||||
|
@ -58,7 +70,12 @@ export abstract class NotificationState
|
|||
}
|
||||
|
||||
public get isUnread(): boolean {
|
||||
return this.color >= NotificationColor.Bold;
|
||||
if (this.color > NotificationColor.Bold) {
|
||||
return true;
|
||||
} else {
|
||||
const hideBold = SettingsStore.getValue("feature_hidebold");
|
||||
return this.color === NotificationColor.Bold && !hideBold;
|
||||
}
|
||||
}
|
||||
|
||||
public get hasUnreadCount(): boolean {
|
||||
|
@ -81,11 +98,15 @@ export abstract class NotificationState
|
|||
|
||||
public destroy(): void {
|
||||
this.removeAllListeners(NotificationStateEvents.Update);
|
||||
for (const watcherReference of this.watcherReferences) {
|
||||
SettingsStore.unwatchSetting(watcherReference);
|
||||
}
|
||||
this.watcherReferences = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationStateSnapshot {
|
||||
private readonly symbol: string;
|
||||
private readonly symbol: string | null;
|
||||
private readonly count: number;
|
||||
private readonly color: NotificationColor;
|
||||
|
||||
|
|
|
@ -98,8 +98,8 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
|||
this.updateNotificationState();
|
||||
};
|
||||
|
||||
private handleRoomEventUpdate = (event: MatrixEvent, room: Room | null) => {
|
||||
if (room?.roomId !== this.room.roomId) return; // ignore - not for us or notifications timeline
|
||||
private handleRoomEventUpdate = (event: MatrixEvent) => {
|
||||
if (event?.getRoomId() !== this.room.roomId) return; // ignore - not for us or notifications timeline
|
||||
this.updateNotificationState();
|
||||
};
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ export class SpaceNotificationState extends NotificationState {
|
|||
super();
|
||||
}
|
||||
|
||||
public get symbol(): string {
|
||||
public get symbol(): string | null {
|
||||
return this._color === NotificationColor.Unsent ? "!" : null;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { NotificationState } from "./NotificationState";
|
|||
export class StaticNotificationState extends NotificationState {
|
||||
public static readonly RED_EXCLAMATION = StaticNotificationState.forSymbol("!", NotificationColor.Red);
|
||||
|
||||
constructor(symbol: string, count: number, color: NotificationColor) {
|
||||
constructor(symbol: string | null, count: number, color: NotificationColor) {
|
||||
super();
|
||||
this._symbol = symbol;
|
||||
this._count = count;
|
||||
|
|
|
@ -20,6 +20,7 @@ import DeviceListener from '../DeviceListener';
|
|||
import GenericToast from "../components/views/toasts/GenericToast";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { snoozeBulkUnverifiedDeviceReminder } from '../utils/device/snoozeBulkUnverifiedDeviceReminder';
|
||||
|
||||
const TOAST_KEY = "reviewsessions";
|
||||
|
||||
|
@ -34,6 +35,7 @@ export const showToast = (deviceIds: Set<string>) => {
|
|||
|
||||
const onReject = () => {
|
||||
DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds);
|
||||
snoozeBulkUnverifiedDeviceReminder();
|
||||
};
|
||||
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
|
|
40
src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts
Normal file
40
src/utils/device/snoozeBulkUnverifiedDeviceReminder.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
const SNOOZE_KEY = 'mx_snooze_bulk_unverified_device_nag';
|
||||
// one week
|
||||
const snoozePeriod = 1000 * 60 * 60 * 24 * 7;
|
||||
export const snoozeBulkUnverifiedDeviceReminder = () => {
|
||||
try {
|
||||
localStorage.setItem(SNOOZE_KEY, String(Date.now()));
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist bulk unverified device nag snooze', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const isBulkUnverifiedDeviceReminderSnoozed = () => {
|
||||
try {
|
||||
const snoozedTimestamp = localStorage.getItem(SNOOZE_KEY);
|
||||
|
||||
const parsedTimestamp = Number.parseInt(snoozedTimestamp || '', 10);
|
||||
|
||||
return Number.isInteger(parsedTimestamp) && (parsedTimestamp + snoozePeriod) > Date.now();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export const VoiceBroadcastRoomSubtitle = () => {
|
||||
return <div className="mx_RoomTile_subtitle mx_RoomTile_subtitle--voice-broadcast">
|
||||
<LiveIcon className="mx_Icon mx_Icon_16" />
|
||||
{ _t("Live") }
|
||||
</div>;
|
||||
};
|
35
src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts
Normal file
35
src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { hasRoomLiveVoiceBroadcast } from "../utils/hasRoomLiveVoiceBroadcast";
|
||||
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
|
||||
|
||||
export const useHasRoomLiveVoiceBroadcast = (room: Room) => {
|
||||
const [hasLiveVoiceBroadcast, setHasLiveVoiceBroadcast] = useState(hasRoomLiveVoiceBroadcast(room).hasBroadcast);
|
||||
|
||||
useTypedEventEmitter(
|
||||
room.currentState,
|
||||
RoomStateEvent.Update,
|
||||
() => {
|
||||
setHasLiveVoiceBroadcast(hasRoomLiveVoiceBroadcast(room).hasBroadcast);
|
||||
},
|
||||
);
|
||||
|
||||
return hasLiveVoiceBroadcast;
|
||||
};
|
|
@ -29,12 +29,14 @@ export * from "./components/VoiceBroadcastBody";
|
|||
export * from "./components/atoms/LiveBadge";
|
||||
export * from "./components/atoms/VoiceBroadcastControl";
|
||||
export * from "./components/atoms/VoiceBroadcastHeader";
|
||||
export * from "./components/atoms/VoiceBroadcastRoomSubtitle";
|
||||
export * from "./components/molecules/VoiceBroadcastPlaybackBody";
|
||||
export * from "./components/molecules/VoiceBroadcastPreRecordingPip";
|
||||
export * from "./components/molecules/VoiceBroadcastRecordingBody";
|
||||
export * from "./components/molecules/VoiceBroadcastRecordingPip";
|
||||
export * from "./hooks/useCurrentVoiceBroadcastPreRecording";
|
||||
export * from "./hooks/useCurrentVoiceBroadcastRecording";
|
||||
export * from "./hooks/useHasRoomLiveVoiceBroadcast";
|
||||
export * from "./hooks/useVoiceBroadcastRecording";
|
||||
export * from "./stores/VoiceBroadcastPlaybacksStore";
|
||||
export * from "./stores/VoiceBroadcastPreRecordingStore";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue