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

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