Merge branch 'develop' into sort-imports
This commit is contained in:
@@ -249,6 +249,8 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||
let handled = true;
|
||||
|
||||
switch (ev.key) {
|
||||
// XXX: this is imitating roving behaviour, it should really use the RovingTabIndex utils
|
||||
// to inherit proper handling of unmount edge cases
|
||||
case Key.TAB:
|
||||
case Key.ESCAPE:
|
||||
case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a <Toolbar />
|
||||
|
||||
@@ -40,6 +40,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
@@ -51,19 +52,12 @@ interface IState {
|
||||
activeSpace?: Room;
|
||||
}
|
||||
|
||||
// List of CSS classes which should be included in keyboard navigation within the room list
|
||||
const cssClasses = [
|
||||
"mx_RoomSearch_input",
|
||||
"mx_RoomSearch_minimizedHandle", // minimized <RoomSearch />
|
||||
"mx_RoomSublist_headerText",
|
||||
"mx_RoomTile",
|
||||
"mx_RoomSublist_showNButton",
|
||||
];
|
||||
|
||||
@replaceableComponent("structures.LeftPanel")
|
||||
export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||
private ref = createRef<HTMLDivElement>();
|
||||
private listContainerRef = createRef<HTMLDivElement>();
|
||||
private roomSearchRef = createRef<RoomSearch>();
|
||||
private roomListRef = createRef<RoomList>();
|
||||
private focusedElement = null;
|
||||
private isDoingStickyHeaders = false;
|
||||
|
||||
@@ -283,16 +277,25 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
this.focusedElement = null;
|
||||
};
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
private onKeyDown = (ev: React.KeyboardEvent, state?: IRovingTabIndexState) => {
|
||||
if (!this.focusedElement) return;
|
||||
|
||||
const action = getKeyBindingsManager().getRoomListAction(ev);
|
||||
switch (action) {
|
||||
case RoomListAction.NextRoom:
|
||||
if (!state) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.roomListRef.current?.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case RoomListAction.PrevRoom:
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.onMoveFocus(action === RoomListAction.PrevRoom);
|
||||
if (state && state.activeRef === findSiblingElement(state.refs, 0)) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.roomSearchRef.current?.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -305,45 +308,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
private onMoveFocus = (up: boolean) => {
|
||||
let element = this.focusedElement;
|
||||
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
let classes: DOMTokenList;
|
||||
|
||||
do {
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
} else if (sibling) {
|
||||
element = sibling;
|
||||
} else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
} else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
} else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
classes = element.classList;
|
||||
}
|
||||
} while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null));
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.focusedElement = element;
|
||||
}
|
||||
};
|
||||
|
||||
private renderHeader(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_LeftPanel_userHeader">
|
||||
@@ -388,7 +352,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
>
|
||||
<RoomSearch
|
||||
isMinimized={this.props.isMinimized}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={this.roomSearchRef}
|
||||
onSelectRoom={this.selectRoom}
|
||||
/>
|
||||
|
||||
@@ -417,6 +381,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
activeSpace={this.state.activeSpace}
|
||||
onResize={this.refreshStickyHeaders}
|
||||
onListCollapse={this.refreshStickyHeaders}
|
||||
ref={this.roomListRef}
|
||||
/>;
|
||||
|
||||
const containerClasses = classNames({
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import React, { ComponentType, createRef } from 'react';
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
@@ -1597,12 +1597,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
|
||||
if (haveNewVersion) {
|
||||
Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
|
||||
import('../../async-components/views/dialogs/security/NewRecoveryMethodDialog'),
|
||||
import(
|
||||
'../../async-components/views/dialogs/security/NewRecoveryMethodDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{ newVersionInfo },
|
||||
);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed',
|
||||
import('../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'),
|
||||
import(
|
||||
'../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -196,6 +196,7 @@ interface IReadReceiptForUser {
|
||||
@replaceableComponent("structures.MessagePanel")
|
||||
export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
// opaque readreceipt info for each userId; used by ReadReceiptMarker
|
||||
// to manage its animations
|
||||
@@ -787,6 +788,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
showReadReceipts={this.props.showReadReceipts}
|
||||
callEventGrouper={callEventGrouper}
|
||||
hideSender={this.membersCount <= 2 && this.props.layout === Layout.Bubble}
|
||||
timelineRenderingType={this.context.timelineRenderingType}
|
||||
/>
|
||||
</TileErrorBoundary>,
|
||||
);
|
||||
|
||||
@@ -32,7 +32,6 @@ import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../.
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
onKeyDown(ev: React.KeyboardEvent): void;
|
||||
/**
|
||||
* @returns true if a room has been selected and the search field should be cleared
|
||||
*/
|
||||
@@ -133,11 +132,6 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
this.clearInput();
|
||||
defaultDispatcher.fire(Action.FocusSendMessageComposer);
|
||||
break;
|
||||
case RoomListAction.NextRoom:
|
||||
case RoomListAction.PrevRoom:
|
||||
// we don't handle these actions here put pass the event on to the interested party (LeftPanel)
|
||||
this.props.onKeyDown(ev);
|
||||
break;
|
||||
case RoomListAction.SelectRoom: {
|
||||
const shouldClear = this.props.onSelectRoom();
|
||||
if (shouldClear) {
|
||||
@@ -151,6 +145,10 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
public focus(): void {
|
||||
this.inputRef.current?.focus();
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const classes = classNames({
|
||||
'mx_RoomSearch': true,
|
||||
|
||||
@@ -93,6 +93,7 @@ import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threads';
|
||||
import { fetchInitialEvent } from "../../utils/EventUtils";
|
||||
import { ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
@@ -863,10 +864,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
}
|
||||
|
||||
case Action.ComposerInsert: {
|
||||
if (payload.composerType) break;
|
||||
// re-dispatch to the correct composer
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: this.state.editState ? "edit_composer_insert" : "send_composer_insert",
|
||||
composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -60,18 +60,15 @@ import { getDisplayAliasForRoom } from "./RoomDirectory";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { useEventEmitterState } from "../../hooks/useEventEmitter";
|
||||
import { IOOBData } from "../../stores/ThreepidInviteStore";
|
||||
import { awaitRoomDownSync } from "../../utils/RoomUpgrade";
|
||||
import RoomViewStore from "../../stores/RoomViewStore";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
initialText?: string;
|
||||
additionalButtons?: ReactNode;
|
||||
showRoom(
|
||||
cli: MatrixClient,
|
||||
hierarchy: RoomHierarchy,
|
||||
roomId: string,
|
||||
autoJoin?: boolean,
|
||||
roomType?: RoomType,
|
||||
): void;
|
||||
showRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, roomType?: RoomType): void;
|
||||
joinRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string): void;
|
||||
}
|
||||
|
||||
interface ITileProps {
|
||||
@@ -80,7 +77,8 @@ interface ITileProps {
|
||||
selected?: boolean;
|
||||
numChildRooms?: number;
|
||||
hasPermissions?: boolean;
|
||||
onViewRoomClick(autoJoin: boolean, roomType: RoomType): void;
|
||||
onViewRoomClick(): void;
|
||||
onJoinRoomClick(): void;
|
||||
onToggleClick?(): void;
|
||||
}
|
||||
|
||||
@@ -91,31 +89,50 @@ const Tile: React.FC<ITileProps> = ({
|
||||
hasPermissions,
|
||||
onToggleClick,
|
||||
onViewRoomClick,
|
||||
onJoinRoomClick,
|
||||
numChildRooms,
|
||||
children,
|
||||
}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
|
||||
const [joinedRoom, setJoinedRoom] = useState<Room>(() => {
|
||||
const cliRoom = cli.getRoom(room.room_id);
|
||||
return cliRoom?.getMyMembership() === "join" ? cliRoom : null;
|
||||
});
|
||||
const joinedRoomName = useEventEmitterState(joinedRoom, "Room.name", room => room?.name);
|
||||
const name = joinedRoomName || room.name || room.canonical_alias || room.aliases?.[0]
|
||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||
|
||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const onPreviewClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(false, room.room_type as RoomType);
|
||||
onViewRoomClick();
|
||||
};
|
||||
const onJoinClick = (ev: ButtonEvent) => {
|
||||
const onJoinClick = async (ev: ButtonEvent) => {
|
||||
setBusy(true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(true, room.room_type as RoomType);
|
||||
onJoinRoomClick();
|
||||
setJoinedRoom(await awaitRoomDownSync(cli, room.room_id));
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
let button;
|
||||
if (joinedRoom) {
|
||||
if (busy) {
|
||||
button = <AccessibleTooltipButton
|
||||
disabled={true}
|
||||
onClick={onJoinClick}
|
||||
kind="primary_outline"
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
title={_t("Joining")}
|
||||
>
|
||||
<Spinner w={24} h={24} />
|
||||
</AccessibleTooltipButton>;
|
||||
} else if (joinedRoom) {
|
||||
button = <AccessibleButton
|
||||
onClick={onPreviewClick}
|
||||
kind="primary_outline"
|
||||
@@ -172,8 +189,15 @@ const Tile: React.FC<ITileProps> = ({
|
||||
description += " · " + topic;
|
||||
}
|
||||
|
||||
let joinedSection;
|
||||
if (joinedRoom) {
|
||||
joinedSection = <div className="mx_SpaceHierarchy_roomTile_joined">
|
||||
{ _t("Joined") }
|
||||
</div>;
|
||||
}
|
||||
|
||||
let suggestedSection;
|
||||
if (suggested) {
|
||||
if (suggested && (!joinedRoom || hasPermissions)) {
|
||||
suggestedSection = <InfoTooltip tooltip={_t("This room is suggested as a good one to join")}>
|
||||
{ _t("Suggested") }
|
||||
</InfoTooltip>;
|
||||
@@ -183,6 +207,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||
{ avatar }
|
||||
<div className="mx_SpaceHierarchy_roomTile_name">
|
||||
{ name }
|
||||
{ joinedSection }
|
||||
{ suggestedSection }
|
||||
</div>
|
||||
|
||||
@@ -274,6 +299,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||
<AccessibleButton
|
||||
className={classNames("mx_SpaceHierarchy_roomTile", {
|
||||
mx_SpaceHierarchy_subspace: room.room_type === RoomType.Space,
|
||||
mx_SpaceHierarchy_joining: busy,
|
||||
})}
|
||||
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
|
||||
onKeyDown={onKeyDown}
|
||||
@@ -288,13 +314,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||
</li>;
|
||||
};
|
||||
|
||||
export const showRoom = (
|
||||
cli: MatrixClient,
|
||||
hierarchy: RoomHierarchy,
|
||||
roomId: string,
|
||||
autoJoin = false,
|
||||
roomType?: RoomType,
|
||||
) => {
|
||||
export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, roomType?: RoomType): void => {
|
||||
const room = hierarchy.roomMap.get(roomId);
|
||||
|
||||
// Don't let the user view a room they won't be able to either peek or join:
|
||||
@@ -309,7 +329,6 @@ export const showRoom = (
|
||||
const roomAlias = getDisplayAliasForRoom(room) || undefined;
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
auto_join: autoJoin,
|
||||
should_peek: true,
|
||||
_type: "room_directory", // instrumentation
|
||||
room_alias: roomAlias,
|
||||
@@ -324,13 +343,29 @@ export const showRoom = (
|
||||
});
|
||||
};
|
||||
|
||||
export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string): void => {
|
||||
// Don't let the user view a room they won't be able to either peek or join:
|
||||
// fail earlier so they don't have to click back to the directory.
|
||||
if (cli.isGuest()) {
|
||||
dis.dispatch({ action: "require_registration" });
|
||||
return;
|
||||
}
|
||||
|
||||
cli.joinRoom(roomId, {
|
||||
viaServers: Array.from(hierarchy.viaMap.get(roomId) || []),
|
||||
}).catch(err => {
|
||||
RoomViewStore.showJoinRoomError(err, roomId);
|
||||
});
|
||||
};
|
||||
|
||||
interface IHierarchyLevelProps {
|
||||
root: IHierarchyRoom;
|
||||
roomSet: Set<IHierarchyRoom>;
|
||||
hierarchy: RoomHierarchy;
|
||||
parents: Set<string>;
|
||||
selectedMap?: Map<string, Set<string>>;
|
||||
onViewRoomClick(roomId: string, autoJoin: boolean, roomType?: RoomType): void;
|
||||
onViewRoomClick(roomId: string, roomType?: RoomType): void;
|
||||
onJoinRoomClick(roomId: string): void;
|
||||
onToggleClick?(parentId: string, childId: string): void;
|
||||
}
|
||||
|
||||
@@ -365,6 +400,7 @@ export const HierarchyLevel = ({
|
||||
parents,
|
||||
selectedMap,
|
||||
onViewRoomClick,
|
||||
onJoinRoomClick,
|
||||
onToggleClick,
|
||||
}: IHierarchyLevelProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
@@ -392,9 +428,8 @@ export const HierarchyLevel = ({
|
||||
room={room}
|
||||
suggested={hierarchy.isSuggested(root.room_id, room.room_id)}
|
||||
selected={selectedMap?.get(root.room_id)?.has(room.room_id)}
|
||||
onViewRoomClick={(autoJoin, roomType) => {
|
||||
onViewRoomClick(room.room_id, autoJoin, roomType);
|
||||
}}
|
||||
onViewRoomClick={() => onViewRoomClick(room.room_id, room.room_type as RoomType)}
|
||||
onJoinRoomClick={() => onJoinRoomClick(room.room_id)}
|
||||
hasPermissions={hasPermissions}
|
||||
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined}
|
||||
/>
|
||||
@@ -412,9 +447,8 @@ export const HierarchyLevel = ({
|
||||
}).length}
|
||||
suggested={hierarchy.isSuggested(root.room_id, space.room_id)}
|
||||
selected={selectedMap?.get(root.room_id)?.has(space.room_id)}
|
||||
onViewRoomClick={(autoJoin, roomType) => {
|
||||
onViewRoomClick(space.room_id, autoJoin, roomType);
|
||||
}}
|
||||
onViewRoomClick={() => onViewRoomClick(space.room_id, RoomType.Space)}
|
||||
onJoinRoomClick={() => onJoinRoomClick(space.room_id)}
|
||||
hasPermissions={hasPermissions}
|
||||
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, space.room_id) : undefined}
|
||||
>
|
||||
@@ -425,6 +459,7 @@ export const HierarchyLevel = ({
|
||||
parents={newParents}
|
||||
selectedMap={selectedMap}
|
||||
onViewRoomClick={onViewRoomClick}
|
||||
onJoinRoomClick={onJoinRoomClick}
|
||||
onToggleClick={onToggleClick}
|
||||
/>
|
||||
</Tile>
|
||||
@@ -537,9 +572,19 @@ const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageBu
|
||||
onClick={async () => {
|
||||
setRemoving(true);
|
||||
try {
|
||||
const userId = cli.getUserId();
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||
|
||||
// remove the child->parent relation too, if we have permission to.
|
||||
const childRoom = cli.getRoom(childId);
|
||||
const parentRelation = childRoom?.currentState.getStateEvents(EventType.SpaceParent, parentId);
|
||||
if (childRoom?.currentState.maySendStateEvent(EventType.SpaceParent, userId) &&
|
||||
Array.isArray(parentRelation?.getContent().via)
|
||||
) {
|
||||
await cli.sendStateEvent(childId, EventType.SpaceParent, {}, parentId);
|
||||
}
|
||||
|
||||
hierarchy.removeRelation(parentId, childId);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -678,9 +723,8 @@ const SpaceHierarchy = ({
|
||||
parents={new Set()}
|
||||
selectedMap={selected}
|
||||
onToggleClick={hasPermissions ? onToggleClick : undefined}
|
||||
onViewRoomClick={(roomId, autoJoin, roomType) => {
|
||||
showRoom(cli, hierarchy, roomId, autoJoin, roomType);
|
||||
}}
|
||||
onViewRoomClick={(roomId, roomType) => showRoom(cli, hierarchy, roomId, roomType)}
|
||||
onJoinRoomClick={(roomId) => joinRoom(cli, hierarchy, roomId)}
|
||||
/>
|
||||
</>;
|
||||
} else if (!hierarchy.canLoadMore) {
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
showSpaceInvite,
|
||||
showSpaceSettings,
|
||||
} from "../../utils/space";
|
||||
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
|
||||
import SpaceHierarchy, { joinRoom, showRoom } from "./SpaceHierarchy";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import FacePile from "../views/elements/FacePile";
|
||||
@@ -507,7 +507,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
||||
) }
|
||||
</RoomTopic>
|
||||
|
||||
<SpaceHierarchy space={space} showRoom={showRoom} additionalButtons={addRoomButton} />
|
||||
<SpaceHierarchy space={space} showRoom={showRoom} joinRoom={joinRoom} additionalButtons={addRoomButton} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
@@ -667,10 +667,6 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
|
||||
<h3>{ _t("Me and my teammates") }</h3>
|
||||
<div>{ _t("A private space for you and your teammates") }</div>
|
||||
</AccessibleButton>
|
||||
<div className="mx_SpaceRoomView_betaWarning">
|
||||
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
|
||||
<p>{ _t("We're working on this, but just want to let you know.") }</p>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,23 +17,22 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from 'classnames';
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import ServerPicker from "../../views/elements/ServerPicker";
|
||||
import EmailField from "../../views/auth/EmailField";
|
||||
import PassphraseField from '../../views/auth/PassphraseField';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
|
||||
import withValidation, { IValidationResult } from "../../views/elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
import { IValidationResult } from "../../views/elements/Validation";
|
||||
import InlineSpinner from '../../views/elements/InlineSpinner';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
enum Phase {
|
||||
// Show the forgot password inputs
|
||||
@@ -227,30 +226,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||
});
|
||||
}
|
||||
|
||||
private validateEmailRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter email address"),
|
||||
}, {
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private onEmailValidate = async (fieldState) => {
|
||||
const result = await this.validateEmailRules(fieldState);
|
||||
|
||||
private onEmailValidate = (result: IValidationResult) => {
|
||||
this.setState({
|
||||
emailFieldValid: result.valid,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private onPasswordValidate(result: IValidationResult) {
|
||||
@@ -302,14 +281,12 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||
/>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<Field
|
||||
<EmailField
|
||||
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||
type="text"
|
||||
label={_t('Email')}
|
||||
value={this.state.email}
|
||||
fieldRef={field => this['email_field'] = field}
|
||||
autoFocus={true}
|
||||
onChange={this.onInputChanged.bind(this, "email")}
|
||||
ref={field => this['email_field'] = field}
|
||||
autoFocus
|
||||
onValidate={this.onEmailValidate}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")}
|
||||
|
||||
92
src/components/views/auth/EmailField.tsx
Normal file
92
src/components/views/auth/EmailField.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2021 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, { PureComponent, RefCallback, RefObject } from "react";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Field, { IInputProps } from "../elements/Field";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
|
||||
interface IProps extends Omit<IInputProps, "onValidate"> {
|
||||
id?: string;
|
||||
fieldRef?: RefCallback<Field> | RefObject<Field>;
|
||||
value: string;
|
||||
autoFocus?: boolean;
|
||||
|
||||
label?: string;
|
||||
labelRequired?: string;
|
||||
labelInvalid?: string;
|
||||
|
||||
// When present, completely overrides the default validation rules.
|
||||
validationRules?: (fieldState: IFieldState) => Promise<IValidationResult>;
|
||||
|
||||
onChange(ev: React.FormEvent<HTMLElement>): void;
|
||||
onValidate?(result: IValidationResult): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.auth.EmailField")
|
||||
class EmailField extends PureComponent<IProps> {
|
||||
static defaultProps = {
|
||||
label: _td("Email"),
|
||||
labelRequired: _td("Enter email address"),
|
||||
labelInvalid: _td("Doesn't look like a valid email address"),
|
||||
};
|
||||
|
||||
public readonly validate = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t(this.props.labelRequired),
|
||||
},
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t(this.props.labelInvalid),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onValidate = async (fieldState: IFieldState) => {
|
||||
let validate = this.validate;
|
||||
if (this.props.validationRules) {
|
||||
validate = this.props.validationRules;
|
||||
}
|
||||
|
||||
const result = await validate(fieldState);
|
||||
if (this.props.onValidate) {
|
||||
this.props.onValidate(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Field
|
||||
id={this.props.id}
|
||||
ref={this.props.fieldRef}
|
||||
type="text"
|
||||
label={_t(this.props.label)}
|
||||
value={this.props.value}
|
||||
autoFocus={this.props.autoFocus}
|
||||
onChange={this.props.onChange}
|
||||
onValidate={this.onValidate}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default EmailField;
|
||||
@@ -22,11 +22,11 @@ import SdkConfig from '../../../SdkConfig';
|
||||
import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import withValidation from "../elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
import withValidation, { IValidationResult } from "../elements/Validation";
|
||||
import Field from "../elements/Field";
|
||||
import CountryDropdown from "./CountryDropdown";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import EmailField from "./EmailField";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
@@ -262,26 +262,8 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
return result;
|
||||
};
|
||||
|
||||
private validateEmailRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter email address"),
|
||||
}, {
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private onEmailValidate = async (fieldState) => {
|
||||
const result = await this.validateEmailRules(fieldState);
|
||||
private onEmailValidate = (result: IValidationResult) => {
|
||||
this.markFieldValid(LoginField.Email, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
private validatePhoneNumberRules = withValidation({
|
||||
@@ -332,12 +314,10 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
switch (loginType) {
|
||||
case LoginField.Email:
|
||||
classes.error = this.props.loginIncorrect && !this.props.username;
|
||||
return <Field
|
||||
return <EmailField
|
||||
className={classNames(classes)}
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
key="email_input"
|
||||
type="text"
|
||||
label={_t("Email")}
|
||||
placeholder="joe@example.com"
|
||||
value={this.props.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
@@ -346,7 +326,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
onValidate={this.onEmailValidate}
|
||||
ref={field => this[LoginField.Email] = field}
|
||||
fieldRef={field => this[LoginField.Email] = field}
|
||||
/>;
|
||||
case LoginField.MatrixId:
|
||||
classes.error = this.props.loginIncorrect && !this.props.username;
|
||||
|
||||
@@ -24,8 +24,9 @@ import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
import withValidation from '../elements/Validation';
|
||||
import withValidation, { IValidationResult } from '../elements/Validation';
|
||||
import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
|
||||
import EmailField from "./EmailField";
|
||||
import PassphraseField from "./PassphraseField";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import Field from '../elements/Field';
|
||||
@@ -252,10 +253,8 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
||||
});
|
||||
};
|
||||
|
||||
private onEmailValidate = async fieldState => {
|
||||
const result = await this.validateEmailRules(fieldState);
|
||||
private onEmailValidate = (result: IValidationResult) => {
|
||||
this.markFieldValid(RegistrationField.Email, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
private validateEmailRules = withValidation({
|
||||
@@ -425,14 +424,14 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
||||
if (!this.showEmail()) {
|
||||
return null;
|
||||
}
|
||||
const emailPlaceholder = this.authStepIsRequired('m.login.email.identity') ?
|
||||
const emailLabel = this.authStepIsRequired('m.login.email.identity') ?
|
||||
_t("Email") :
|
||||
_t("Email (optional)");
|
||||
return <Field
|
||||
ref={field => this[RegistrationField.Email] = field}
|
||||
type="text"
|
||||
label={emailPlaceholder}
|
||||
return <EmailField
|
||||
fieldRef={field => this[RegistrationField.Email] = field}
|
||||
label={emailLabel}
|
||||
value={this.state.email}
|
||||
validationRules={this.validateEmailRules.bind(this)}
|
||||
onChange={this.onEmailChange}
|
||||
onValidate={this.onEmailValidate}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email_focus")}
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import classNames from "classnames";
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
@@ -83,8 +84,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: type when js-sdk has types
|
||||
private onRoomStateEvents = (ev: any) => {
|
||||
private onRoomStateEvents = (ev: MatrixEvent) => {
|
||||
if (!this.props.room ||
|
||||
ev.getRoomId() !== this.props.room.roomId ||
|
||||
ev.getType() !== 'm.room.avatar'
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@@ -85,7 +85,9 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
import(
|
||||
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
@@ -111,7 +113,9 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||
);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog"),
|
||||
import(
|
||||
"../../../async-components/views/dialogs/security/CreateKeyBackupDialog"
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,25 +21,14 @@ import { _t } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import Field from "../elements/Field";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import withValidation from "../elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import EmailField from "../auth/EmailField";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
onFinished(continued: boolean, email?: string): void;
|
||||
}
|
||||
|
||||
const validation = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const fieldRef = useRef<Field>();
|
||||
@@ -47,11 +36,11 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (email) {
|
||||
const valid = await fieldRef.current.validate({ allowEmpty: false });
|
||||
const valid = await fieldRef.current.validate({});
|
||||
|
||||
if (!valid) {
|
||||
fieldRef.current.focus();
|
||||
fieldRef.current.validate({ allowEmpty: false, focused: true });
|
||||
fieldRef.current.validate({ focused: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -72,16 +61,15 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
}) }</p>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Field
|
||||
ref={fieldRef}
|
||||
<EmailField
|
||||
fieldRef={fieldRef}
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
label={_t("Email (optional)")}
|
||||
value={email}
|
||||
onChange={ev => {
|
||||
setEmail(ev.target.value);
|
||||
const target = ev.target as HTMLInputElement;
|
||||
setEmail(target.value);
|
||||
}}
|
||||
onValidate={async fieldState => await validation(fieldState)}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email2_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")}
|
||||
/>
|
||||
|
||||
@@ -44,6 +44,7 @@ import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog";
|
||||
import EditMessageComposer from '../rooms/EditMessageComposer';
|
||||
import LinkPreviewGroup from '../rooms/LinkPreviewGroup';
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
|
||||
const MAX_HIGHLIGHT_LENGTH = 4096;
|
||||
|
||||
@@ -62,6 +63,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||
private unmounted = false;
|
||||
private pills: Element[] = [];
|
||||
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -406,6 +410,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: mxEvent.getSender(),
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialo
|
||||
import { bulkSpaceBehaviour } from "../../../utils/space";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
|
||||
export interface IDevice {
|
||||
deviceId: string;
|
||||
@@ -377,6 +378,7 @@ const UserOptionsSection: React.FC<{
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: member.userId,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
formatRangeAsCode,
|
||||
toggleInlineFormat,
|
||||
replaceRangeAndMoveCaret,
|
||||
formatRangeAsLink,
|
||||
} from '../../../editor/operations';
|
||||
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
|
||||
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
|
||||
@@ -476,6 +477,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||
switch (autocompleteAction) {
|
||||
case AutocompleteAction.ForceComplete:
|
||||
case AutocompleteAction.Complete:
|
||||
this.historyManager.ensureLastChangesPushed(this.props.model);
|
||||
this.modifiedFlag = true;
|
||||
autoComplete.confirmCompletion();
|
||||
handled = true;
|
||||
break;
|
||||
@@ -705,6 +708,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||
case Formatting.Quote:
|
||||
formatRangeAsQuote(range);
|
||||
break;
|
||||
case Formatting.InsertLink:
|
||||
formatRangeAsLink(range);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
|
||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const html = mxEvent.getContent().formatted_body;
|
||||
@@ -498,7 +499,12 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === "edit_composer_insert" && this.editorRef.current) {
|
||||
if (!this.editorRef.current) return;
|
||||
|
||||
if (payload.action === Action.ComposerInsert) {
|
||||
if (payload.timelineRenderingType !== this.context.timelineRenderingType) return;
|
||||
if (payload.composerType !== ComposerType.Edit) return;
|
||||
|
||||
if (payload.userId) {
|
||||
this.editorRef.current?.insertMention(payload.userId);
|
||||
} else if (payload.event) {
|
||||
@@ -506,7 +512,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
||||
} else if (payload.text) {
|
||||
this.editorRef.current?.insertPlaintext(payload.text);
|
||||
}
|
||||
} else if (payload.action === Action.FocusEditMessageComposer && this.editorRef.current) {
|
||||
} else if (payload.action === Action.FocusEditMessageComposer) {
|
||||
this.editorRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,7 +22,6 @@ import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import ReplyChain from "../elements/ReplyChain";
|
||||
import { _t } from '../../../languageHandler';
|
||||
@@ -62,6 +61,9 @@ import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
|
||||
import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads';
|
||||
import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore';
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
|
||||
const eventTileTypes = {
|
||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||
[EventType.Sticker]: 'messages.MessageEvent',
|
||||
@@ -312,6 +314,8 @@ interface IProps {
|
||||
|
||||
// whether or not to display thread info
|
||||
showThreadInfo?: boolean;
|
||||
|
||||
timelineRenderingType?: TimelineRenderingType;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -854,10 +858,11 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
onSenderProfileClick = () => {
|
||||
const mxEvent = this.props.mxEvent;
|
||||
if (!this.props.timelineRenderingType) return;
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: mxEvent.getSender(),
|
||||
userId: this.props.mxEvent.getSender(),
|
||||
timelineRenderingType: this.props.timelineRenderingType,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1090,7 +1095,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
if (needsSenderProfile && this.props.hideSender !== true) {
|
||||
if (!this.props.tileShape) {
|
||||
if (!this.props.tileShape || this.props.tileShape === TileShape.Thread) {
|
||||
sender = <SenderProfile onClick={this.onSenderProfileClick}
|
||||
mxEvent={this.props.mxEvent}
|
||||
enableFlair={this.props.enableFlair}
|
||||
|
||||
@@ -253,7 +253,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
|
||||
public static contextType = RoomContext;
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
static defaultProps = {
|
||||
compact: false,
|
||||
@@ -399,13 +400,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
private addEmoji(emoji: string): boolean {
|
||||
private addEmoji = (emoji: string): boolean => {
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
text: emoji,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private sendMessage = async () => {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton.current) {
|
||||
|
||||
@@ -27,6 +27,7 @@ export enum Formatting {
|
||||
Strikethrough = "strikethrough",
|
||||
Code = "code",
|
||||
Quote = "quote",
|
||||
InsertLink = "insert_link",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
@@ -57,6 +58,7 @@ export default class MessageComposerFormatBar extends React.PureComponent<IProps
|
||||
<FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction(Formatting.Strikethrough)} icon="Strikethrough" visible={this.state.visible} />
|
||||
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction(Formatting.Code)} icon="Code" visible={this.state.visible} />
|
||||
<FormatButton label={_t("Quote")} onClick={() => this.props.onAction(Formatting.Quote)} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
|
||||
<FormatButton label={_t("Insert link")} onClick={() => this.props.onAction(Formatting.InsertLink)} icon="InsertLink" visible={this.state.visible} />
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactComponentElement } from "react";
|
||||
import React, { createRef, ReactComponentElement } from "react";
|
||||
import { Dispatcher } from "flux";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
@@ -22,7 +22,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
import { RovingTabIndexProvider, IState as IRovingTabIndexState } from "../../../accessibility/RovingTabIndex";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
@@ -54,7 +54,7 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
|
||||
onFocus: (ev: React.FocusEvent) => void;
|
||||
onBlur: (ev: React.FocusEvent) => void;
|
||||
onResize: () => void;
|
||||
@@ -249,6 +249,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef;
|
||||
private customTagStoreRef;
|
||||
private roomStoreToken: fbEmitter.EventSubscription;
|
||||
private treeRef = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
@@ -505,6 +506,12 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
});
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
// focus the first focusable element in this aria treeview widget
|
||||
[...this.treeRef.current?.querySelectorAll<HTMLElement>('[role="treeitem"]')]
|
||||
.find(e => e.offsetParent !== null)?.focus();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const userId = cli.getUserId();
|
||||
@@ -584,7 +591,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
|
||||
const sublists = this.renderSublists();
|
||||
return (
|
||||
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
|
||||
<RovingTabIndexProvider handleHomeEnd handleUpDown onKeyDown={this.props.onKeyDown}>
|
||||
{ ({ onKeyDownHandler }) => (
|
||||
<div
|
||||
onFocus={this.props.onFocus}
|
||||
@@ -593,6 +600,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
className="mx_RoomList"
|
||||
role="tree"
|
||||
aria-label={_t("Rooms")}
|
||||
ref={this.treeRef}
|
||||
>
|
||||
{ sublists }
|
||||
{ explorePrompt }
|
||||
|
||||
@@ -58,6 +58,7 @@ import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
import DocumentPosition from "../../../editor/position";
|
||||
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
|
||||
function addReplyToMessageContent(
|
||||
content: IContent,
|
||||
@@ -591,7 +592,10 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
||||
this.editorRef.current?.focus();
|
||||
}
|
||||
break;
|
||||
case "send_composer_insert":
|
||||
case Action.ComposerInsert:
|
||||
if (payload.timelineRenderingType !== this.context.timelineRenderingType) break;
|
||||
if (payload.composerType !== ComposerType.Send) break;
|
||||
|
||||
if (payload.userId) {
|
||||
this.editorRef.current?.insertMention(payload.userId);
|
||||
} else if (payload.event) {
|
||||
|
||||
@@ -15,10 +15,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import Field from "../elements/Field";
|
||||
import React, { ComponentType } from 'react';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Spinner from '../elements/Spinner';
|
||||
@@ -29,6 +27,7 @@ import PassphraseField from "../auth/PassphraseField";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import SetEmailDialog from "../dialogs/SetEmailDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
|
||||
@@ -187,7 +186,9 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
import(
|
||||
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
@@ -92,14 +92,18 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
|
||||
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
import(
|
||||
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{ matrixClient: MatrixClientPeg.get() },
|
||||
);
|
||||
};
|
||||
|
||||
private onImportE2eKeysClicked = (): void => {
|
||||
Modal.createTrackedDialogAsync('Import E2E Keys', '',
|
||||
import('../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
|
||||
import(
|
||||
'../../../async-components/views/dialogs/security/ImportE2eKeysDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{ matrixClient: MatrixClientPeg.get() },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,10 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
@@ -30,6 +27,8 @@ import QuestionDialog from '../dialogs/QuestionDialog';
|
||||
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
|
||||
import { accessSecretStorage } from '../../../SecurityManager';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
|
||||
|
||||
interface IState {
|
||||
loading: boolean;
|
||||
@@ -44,6 +43,8 @@ interface IState {
|
||||
sessionsRemaining: number;
|
||||
}
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@replaceableComponent("views.settings.SecureBackupPanel")
|
||||
export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||
private unmounted = false;
|
||||
@@ -169,7 +170,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||
|
||||
private startNewBackup = (): void => {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
|
||||
import(
|
||||
'../../../async-components/views/dialogs/security/CreateKeyBackupDialog'
|
||||
) as unknown as Promise<ComponentType<{}>>,
|
||||
{
|
||||
onFinished: () => {
|
||||
this.loadBackupStatus();
|
||||
|
||||
@@ -19,7 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { enumerateThemes } from "../../../theme";
|
||||
import { enumerateThemes, findHighContrastTheme, findNonHighContrastTheme, isHighContrastTheme } from "../../../theme";
|
||||
import ThemeWatcher from "../../../settings/watchers/ThemeWatcher";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
@@ -159,7 +159,37 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
||||
this.setState({ customThemeUrl: e.target.value });
|
||||
};
|
||||
|
||||
public render() {
|
||||
private renderHighContrastCheckbox(): React.ReactElement<HTMLDivElement> {
|
||||
if (
|
||||
!this.state.useSystemTheme && (
|
||||
findHighContrastTheme(this.state.theme) ||
|
||||
isHighContrastTheme(this.state.theme)
|
||||
)
|
||||
) {
|
||||
return <div>
|
||||
<StyledCheckbox
|
||||
checked={isHighContrastTheme(this.state.theme)}
|
||||
onChange={(e) => this.highContrastThemeChanged(e.target.checked)}
|
||||
>
|
||||
{ _t( "Use high contrast" ) }
|
||||
</StyledCheckbox>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
private highContrastThemeChanged(checked: boolean): void {
|
||||
let newTheme: string;
|
||||
if (checked) {
|
||||
newTheme = findHighContrastTheme(this.state.theme);
|
||||
} else {
|
||||
newTheme = findNonHighContrastTheme(this.state.theme);
|
||||
}
|
||||
if (newTheme) {
|
||||
this.onThemeChange(newTheme);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<HTMLDivElement> {
|
||||
const themeWatcher = new ThemeWatcher();
|
||||
let systemThemeSection: JSX.Element;
|
||||
if (themeWatcher.isSystemThemeSupported()) {
|
||||
@@ -210,7 +240,8 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
||||
|
||||
// XXX: replace any type here
|
||||
const themes = Object.entries<any>(enumerateThemes())
|
||||
.map(p => ({ id: p[0], name: p[1] })); // convert pairs to objects for code readability
|
||||
.map(p => ({ id: p[0], name: p[1] })) // convert pairs to objects for code readability
|
||||
.filter(p => !isHighContrastTheme(p.id));
|
||||
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
||||
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
||||
.sort((a, b) => compare(a.name, b.name));
|
||||
@@ -229,12 +260,21 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
||||
className: "mx_ThemeSelector_" + t.id,
|
||||
}))}
|
||||
onChange={this.onThemeChange}
|
||||
value={this.state.useSystemTheme ? undefined : this.state.theme}
|
||||
value={this.apparentSelectedThemeId()}
|
||||
outlined
|
||||
/>
|
||||
</div>
|
||||
{ this.renderHighContrastCheckbox() }
|
||||
{ customThemeForm }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
apparentSelectedThemeId() {
|
||||
if (this.state.useSystemTheme) {
|
||||
return undefined;
|
||||
}
|
||||
const nonHighContrast = findNonHighContrastTheme(this.state.theme);
|
||||
return nonHighContrast ? nonHighContrast : this.state.theme;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ import SpaceStore, {
|
||||
} from "../../../stores/SpaceStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
@@ -228,75 +227,12 @@ const SpacePanel = () => {
|
||||
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
|
||||
}, []);
|
||||
|
||||
const onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
if (ev.defaultPrevented) return;
|
||||
|
||||
let handled = true;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_UP:
|
||||
onMoveFocus(ev.target as Element, true);
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
onMoveFocus(ev.target as Element, false);
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
// consume all other keys in context menu
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onMoveFocus = (element: Element, up: boolean) => {
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
let classes: DOMTokenList;
|
||||
|
||||
do {
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
} else if (sibling) {
|
||||
element = sibling;
|
||||
} else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
} else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
} else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
|
||||
element = up ? element.lastElementChild : element.firstElementChild;
|
||||
descending = true;
|
||||
}
|
||||
classes = element.classList;
|
||||
}
|
||||
} while (element && !classes.contains("mx_SpaceButton"));
|
||||
|
||||
if (element) {
|
||||
(element as HTMLElement).focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={result => {
|
||||
if (!result.destination) return; // dropped outside the list
|
||||
SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
|
||||
}}>
|
||||
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||
<RovingTabIndexProvider handleHomeEnd handleUpDown>
|
||||
{ ({ onKeyDownHandler }) => (
|
||||
<ul
|
||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||
|
||||
@@ -279,6 +279,8 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
if (window.electron?.getDesktopCapturerSources) {
|
||||
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||
const [source] = await finished;
|
||||
if (!source) return;
|
||||
|
||||
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
|
||||
} else {
|
||||
isScreensharing = await this.props.call.setScreensharingEnabled(true);
|
||||
@@ -545,6 +547,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
<div
|
||||
className={classes}
|
||||
onMouseMove={this.onMouseMove}
|
||||
ref={this.contentRef}
|
||||
>
|
||||
{ sidebar }
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
|
||||
Reference in New Issue
Block a user