Merge branch 'develop' into sort-imports

Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
Aaron Raimist 2021-12-09 08:34:20 +00:00
commit 7b94e13a84
642 changed files with 30052 additions and 8035 deletions

View file

@ -16,10 +16,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import { _t } from '../../../languageHandler';
import { ContextMenu, IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
import ContextMenu, { IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import CallHandler from '../../../CallHandler';
import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog';
import Modal from '../../../Modal';
@ -46,7 +45,7 @@ export default class CallContextMenu extends React.Component<IProps> {
};
onUnholdClick = () => {
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
CallHandler.instance.setActiveCallRoomId(this.props.call.roomId);
this.props.onFinished();
};

View file

@ -16,10 +16,9 @@ limitations under the License.
import * as React from "react";
import { createRef } from "react";
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
import ContextMenu, { IProps as IContextMenuProps } from '../../structures/ContextMenu';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import Field from "../elements/Field";
import DialPad from '../voip/DialPad';
import { replaceableComponent } from "../../../utils/replaceableComponent";

View file

@ -17,13 +17,13 @@ limitations under the License.
import React from "react";
import classNames from "classnames";
import {
import ContextMenu, {
ChevronFace,
ContextMenu,
IProps as IContextMenuProps,
MenuItem,
MenuItemCheckbox, MenuItemRadio,
} from "../../structures/ContextMenu";
import { _t } from "../../../languageHandler";
interface IProps extends IContextMenuProps {
className?: string;
@ -42,6 +42,7 @@ interface IOptionProps extends React.ComponentProps<typeof MenuItem> {
interface ICheckboxProps extends React.ComponentProps<typeof MenuItemCheckbox> {
iconClassName: string;
words?: boolean;
}
interface IRadioProps extends React.ComponentProps<typeof MenuItemRadio> {
@ -74,8 +75,21 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
iconClassName,
active,
className,
words,
...props
}) => {
let marker: JSX.Element;
if (words) {
marker = <span className="mx_IconizedContextMenu_activeText">
{ active ? _t("On") : _t("Off") }
</span>;
} else {
marker = <span className={classNames("mx_IconizedContextMenu_icon", {
mx_IconizedContextMenu_checked: active,
mx_IconizedContextMenu_unchecked: !active,
})} />;
}
return <MenuItemCheckbox
{...props}
className={classNames(className, {
@ -86,10 +100,7 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{ label }</span>
<span className={classNames("mx_IconizedContextMenu_icon", {
mx_IconizedContextMenu_checked: active,
mx_IconizedContextMenu_unchecked: !active,
})} />
{ marker }
</MenuItemCheckbox>;
};

View file

@ -39,6 +39,12 @@ import ShareDialog from '../dialogs/ShareDialog';
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { IPosition, ChevronFace } from '../../structures/ContextMenu';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
import { POLL_START_EVENT_TYPE } from '../../../polls/consts';
import EndPollDialog from '../dialogs/EndPollDialog';
import { Relations } from 'matrix-js-sdk/src/models/relations';
import { isPollEnded } from '../messages/MPollBody';
export function canCancel(eventStatus: EventStatus): boolean {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -66,6 +72,11 @@ interface IProps extends IPosition {
onFinished(): void;
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
onCloseDialog?(): void;
getRelationsForEvent?: (
eventId: string,
relationType: string,
eventType: string
) => Relations;
}
interface IState {
@ -76,6 +87,7 @@ interface IState {
@replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
state = {
canRedact: false,
@ -120,6 +132,14 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
}
private canEndPoll(mxEvent: MatrixEvent): boolean {
return (
mxEvent.getType() === POLL_START_EVENT_TYPE.name &&
this.state.canRedact &&
!isPollEnded(mxEvent, MatrixClientPeg.get(), this.props.getRelationsForEvent)
);
}
private onResendReactionsClick = (): void => {
for (const reaction of this.getUnsentReactions()) {
Resend.resend(reaction);
@ -190,9 +210,10 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
};
private onQuoteClick = (): void => {
dis.dispatch({
dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert,
event: this.props.mxEvent,
timelineRenderingType: this.context.timelineRenderingType,
});
this.closeMenu();
};
@ -211,6 +232,16 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu();
};
private onEndPollClick = (): void => {
const matrixClient = MatrixClientPeg.get();
Modal.createTrackedDialog('End Poll', '', EndPollDialog, {
matrixClient,
event: this.props.mxEvent,
getRelationsForEvent: this.props.getRelationsForEvent,
}, 'mx_Dialog_endPoll');
this.closeMenu();
};
private getReactions(filter: (e: MatrixEvent) => boolean): MatrixEvent[] {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
@ -231,7 +262,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
private viewInRoom = () => {
dis.dispatch({
action: 'view_room',
action: Action.ViewRoom,
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
@ -246,6 +277,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
const eventStatus = mxEvent.status;
const unsentReactionsCount = this.getUnsentReactions().length;
let endPollButton: JSX.Element;
let resendReactionsButton: JSX.Element;
let redactButton: JSX.Element;
let forwardButton: JSX.Element;
@ -341,6 +373,16 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
/>
);
if (this.canEndPoll(mxEvent)) {
endPollButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconEndPoll"
label={_t("End Poll")}
onClick={this.onEndPollClick}
/>
);
}
if (this.props.eventTileOps) { // this event is rendered using TextualBody
quoteButton = (
<IconizedContextMenuOption
@ -401,13 +443,17 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
const isThreadRootEvent = isThread && this.props.mxEvent?.getThread()?.rootEvent === this.props.mxEvent;
const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget(
MatrixClientPeg.get().getRoom(mxEvent.getRoomId()),
);
const commonItemsList = (
<IconizedContextMenuOptionList>
{ isThreadRootEvent && <IconizedContextMenuOption
{ (isThreadRootEvent && isMainSplitTimelineShown) && <IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconViewInRoom"
label={_t("View in room")}
onClick={this.viewInRoom}
/> }
{ endPollButton }
{ quoteButton }
{ forwardButton }
{ pinButton }

View file

@ -0,0 +1,313 @@
/*
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, { useContext } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuCheckbox,
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "./IconizedContextMenu";
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ButtonEvent } from "../elements/AccessibleButton";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import dis from "../../../dispatcher/dispatcher";
import RoomListActions from "../../../actions/RoomListActions";
import { Key } from "../../../Keyboard";
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
import { RoomNotifState } from "../../../RoomNotifs";
import Modal from "../../../Modal";
import ExportDialog from "../dialogs/ExportDialog";
import { onRoomFilesClick, onRoomMembersClick } from "../right_panel/RoomSummaryCard";
import RoomViewStore from "../../../stores/RoomViewStore";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import { Action } from "../../../dispatcher/actions";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
interface IProps extends IContextMenuProps {
room: Room;
}
const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
const cli = useContext(MatrixClientContext);
const roomTags = useEventEmitterState(
RoomListStore.instance,
LISTS_UPDATE_EVENT,
() => RoomListStore.instance.getTagsForRoom(room),
);
let leaveOption: JSX.Element;
if (roomTags.includes(DefaultTagID.Archived)) {
const onForgetRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: "forget_room",
room_id: room.roomId,
});
onFinished();
};
leaveOption = <IconizedContextMenuOption
iconClassName="mx_RoomTile_iconSignOut"
label={_t("Forget")}
className="mx_IconizedContextMenu_option_red"
onClick={onForgetRoomClick}
/>;
} else {
const onLeaveRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: "leave_room",
room_id: room.roomId,
});
onFinished();
};
leaveOption = <IconizedContextMenuOption
onClick={onLeaveRoomClick}
label={_t("Leave")}
className="mx_IconizedContextMenu_option_red"
iconClassName="mx_RoomTile_iconSignOut"
/>;
}
let inviteOption: JSX.Element;
if (room.canInvite(cli.getUserId())) {
const onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: "view_invite",
roomId: room.roomId,
});
onFinished();
};
inviteOption = <IconizedContextMenuOption
onClick={onInviteClick}
label={_t("Invite")}
iconClassName="mx_RoomTile_iconInvite"
/>;
}
let favouriteOption: JSX.Element;
let lowPriorityOption: JSX.Element;
let notificationOption: JSX.Element;
if (room.getMyMembership() === "join") {
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
favouriteOption = <IconizedContextMenuCheckbox
onClick={(e) => onTagRoom(e, DefaultTagID.Favourite)}
active={isFavorite}
label={isFavorite ? _t("Favourited") : _t("Favourite")}
iconClassName="mx_RoomTile_iconStar"
/>;
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
lowPriorityOption = <IconizedContextMenuCheckbox
onClick={(e) => onTagRoom(e, DefaultTagID.LowPriority)}
active={isLowPriority}
label={_t("Low priority")}
iconClassName="mx_RoomTile_iconArrowDown"
/>;
const echoChamber = EchoChamber.forRoom(room);
let notificationLabel: string;
let iconClassName: string;
switch (echoChamber.notificationVolume) {
case RoomNotifState.AllMessages:
notificationLabel = _t("Default");
iconClassName = "mx_RoomTile_iconNotificationsDefault";
break;
case RoomNotifState.AllMessagesLoud:
notificationLabel = _t("All messages");
iconClassName = "mx_RoomTile_iconNotificationsAllMessages";
break;
case RoomNotifState.MentionsOnly:
notificationLabel = _t("Mentions only");
iconClassName = "mx_RoomTile_iconNotificationsMentionsKeywords";
break;
case RoomNotifState.Mute:
notificationLabel = _t("Mute");
iconClassName = "mx_RoomTile_iconNotificationsNone";
break;
}
notificationOption = <IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: "open_room_settings",
room_id: room.roomId,
initial_tab_id: ROOM_NOTIFICATIONS_TAB,
});
onFinished();
}}
label={_t("Notifications")}
iconClassName={iconClassName}
>
<span className="mx_IconizedContextMenu_sublabel">
{ notificationLabel }
</span>
</IconizedContextMenuOption>;
}
const onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
ev.preventDefault();
ev.stopPropagation();
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
const isApplied = RoomListStore.instance.getTagsForRoom(room).includes(tagId);
const removeTag = isApplied ? tagId : inverseTag;
const addTag = isApplied ? null : tagId;
dis.dispatch(RoomListActions.tagRoom(cli, room, removeTag, addTag, undefined, 0));
} else {
logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`);
}
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
onFinished();
}
};
const ensureViewingRoom = () => {
if (RoomViewStore.getRoomId() === room.roomId) return;
dis.dispatch({
action: "view_room",
room_id: room.roomId,
}, true);
};
return <IconizedContextMenu {...props} onFinished={onFinished} className="mx_RoomTile_contextMenu" compact>
<IconizedContextMenuOptionList>
{ inviteOption }
{ notificationOption }
{ favouriteOption }
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
ensureViewingRoom();
onRoomMembersClick(false);
onFinished();
}}
label={_t("People")}
iconClassName="mx_RoomTile_iconPeople"
>
<span className="mx_IconizedContextMenu_sublabel">
{ room.getJoinedMemberCount() }
</span>
</IconizedContextMenuOption>
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
ensureViewingRoom();
onRoomFilesClick(false);
onFinished();
}}
label={_t("Files")}
iconClassName="mx_RoomTile_iconFiles"
/>
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
ensureViewingRoom();
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomSummary,
allowClose: false,
});
onFinished();
}}
label={_t("Widgets")}
iconClassName="mx_RoomTile_iconWidgets"
/>
{ lowPriorityOption }
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: "copy_room",
room_id: room.roomId,
});
onFinished();
}}
label={_t("Copy link")}
iconClassName="mx_RoomTile_iconCopyLink"
/>
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: "open_room_settings",
room_id: room.roomId,
});
onFinished();
}}
label={_t("Settings")}
iconClassName="mx_RoomTile_iconSettings"
/>
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Export room dialog', '', ExportDialog, { room });
onFinished();
}}
label={_t("Export chat")}
iconClassName="mx_RoomTile_iconExport"
/>
{ leaveOption }
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
};
export default RoomContextMenu;

View file

@ -26,7 +26,6 @@ import { _t } from "../../../languageHandler";
import {
leaveSpace,
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
showCreateNewSubspace,
showSpaceInvite,
@ -35,18 +34,16 @@ import {
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomViewStore from "../../../stores/RoomViewStore";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import { Action } from "../../../dispatcher/actions";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import { BetaPill } from "../beta/BetaCard";
import SettingsStore from "../../../settings/SettingsStore";
import { Action } from "../../../dispatcher/actions";
interface IProps extends IContextMenuProps {
space: Room;
hideHeader?: boolean;
}
const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) => {
const cli = useContext(MatrixClientContext);
const userId = cli.getUserId();
@ -64,14 +61,14 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
<IconizedContextMenuOption
className="mx_SpacePanel_contextMenu_inviteButton"
iconClassName="mx_SpacePanel_iconInvite"
label={_t("Invite people")}
label={_t("Invite")}
onClick={onInviteClick}
/>
);
}
let settingsOption;
let leaveSection;
let leaveOption;
if (shouldShowSpaceSettings(space)) {
const onSettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
@ -97,36 +94,37 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
onFinished();
};
leaveSection = <IconizedContextMenuOptionList red first>
leaveOption = (
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconLeave"
className="mx_IconizedContextMenu_option_red"
label={_t("Leave space")}
onClick={onLeaveClick}
/>
</IconizedContextMenuOptionList>;
);
}
let devtoolsSection;
let devtoolsOption;
if (SettingsStore.getValue("developerMode")) {
const onViewTimelineClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch({
action: 'view_room',
action: Action.ViewRoom,
room_id: space.roomId,
forceTimeline: true,
});
onFinished();
};
devtoolsSection = <IconizedContextMenuOptionList first>
devtoolsOption = (
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconSettings"
label={_t("See room timeline (devtools)")}
onClick={onViewTimelineClick}
/>
</IconizedContextMenuOptionList>;
);
}
const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
@ -141,14 +139,6 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
onFinished();
};
const onAddExistingRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showAddExistingRooms(space);
onFinished();
};
const onNewSubspaceClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -157,46 +147,25 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
onFinished();
};
newRoomSection = <IconizedContextMenuOptionList first>
newRoomSection = <>
<div className="mx_SpacePanel_contextMenu_separatorLabel">
{ _t("Add") }
</div>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Create new room")}
label={_t("Room")}
onClick={onNewRoomClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHash"
label={_t("Add existing room")}
onClick={onAddExistingRoomClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Add space")}
label={_t("Space")}
onClick={onNewSubspaceClick}
>
<BetaPill />
</IconizedContextMenuOption>
</IconizedContextMenuOptionList>;
</>;
}
const onMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
if (!RoomViewStore.getRoomId()) {
defaultDispatcher.dispatch({
action: "view_room",
room_id: space.roomId,
}, true);
}
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.SpaceMemberList,
refireParams: { space },
});
onFinished();
};
const onExploreRoomsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -214,26 +183,21 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
className="mx_SpacePanel_contextMenu"
compact
>
<div className="mx_SpacePanel_contextMenu_header">
{ !hideHeader && <div className="mx_SpacePanel_contextMenu_header">
{ space.name }
</div>
</div> }
<IconizedContextMenuOptionList first>
{ inviteOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconMembers"
label={_t("Members")}
onClick={onMembersClick}
/>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore"
label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
onClick={onExploreRoomsClick}
/>
{ settingsOption }
{ leaveOption }
{ devtoolsOption }
{ newRoomSection }
</IconizedContextMenuOptionList>
{ newRoomSection }
{ leaveSection }
{ devtoolsSection }
</IconizedContextMenu>;
};

View file

@ -1,156 +1 @@
/*
Copyright 2018 New Vector Ltd
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, { ChangeEvent } from 'react';
import { User } from "matrix-js-sdk/src/models/user";
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "../elements/Spinner";
interface IProps {
// js-sdk User object. Not required because it might not exist.
user?: User;
}
interface IState {
message: string;
waiting: boolean;
}
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
export default class StatusMessageContextMenu extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
message: this.comittedStatusMessage,
waiting: false,
};
}
public componentDidMount(): void {
const { user } = this.props;
if (!user) {
return;
}
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
}
public componentWillUnmount(): void {
const { user } = this.props;
if (!user) {
return;
}
user.removeListener(
"User._unstable_statusMessage",
this.onStatusMessageCommitted,
);
}
get comittedStatusMessage(): string {
return this.props.user ? this.props.user.unstable_statusMessage : "";
}
private onStatusMessageCommitted = (): void => {
// The `User` object has observed a status message change.
this.setState({
message: this.comittedStatusMessage,
waiting: false,
});
};
private onClearClick = (): void=> {
MatrixClientPeg.get()._unstable_setStatusMessage("");
this.setState({
waiting: true,
});
};
private onSubmit = (e: ButtonEvent): void => {
e.preventDefault();
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
this.setState({
waiting: true,
});
};
private onStatusChange = (e: ChangeEvent): void => {
// The input field's value was changed.
this.setState({
message: (e.target as HTMLInputElement).value,
});
};
public render(): JSX.Element {
let actionButton;
if (this.comittedStatusMessage) {
if (this.state.message === this.comittedStatusMessage) {
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
onClick={this.onClearClick}
>
<span>{ _t("Clear status") }</span>
</AccessibleButton>;
} else {
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
onClick={this.onSubmit}
>
<span>{ _t("Update status") }</span>
</AccessibleButton>;
}
} else {
actionButton = <AccessibleButton
className="mx_StatusMessageContextMenu_submit"
disabled={!this.state.message}
onClick={this.onSubmit}
>
<span>{ _t("Set status") }</span>
</AccessibleButton>;
}
let spinner = null;
if (this.state.waiting) {
spinner = <Spinner w={24} h={24} />;
}
const form = <form
className="mx_StatusMessageContextMenu_form"
autoComplete="off"
onSubmit={this.onSubmit}
>
<input
type="text"
className="mx_StatusMessageContextMenu_message"
key="message"
placeholder={_t("Set a new status...")}
autoFocus={true}
maxLength={60}
value={this.state.message}
onChange={this.onStatusChange}
/>
<div className="mx_StatusMessageContextMenu_actionContainer">
{ actionButton }
{ spinner }
</div>
</form>;
return <div className="mx_StatusMessageContextMenu">
{ form }
</div>;
}
}

View file

@ -0,0 +1,119 @@
/*
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, { useCallback, useEffect, useState } from "react";
import { MatrixEvent } from "matrix-js-sdk/src";
import { ButtonEvent } from "../elements/AccessibleButton";
import dis from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { copyPlaintext } from "../../../utils/strings";
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
import { _t } from "../../../languageHandler";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
interface IProps {
mxEvent: MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
onMenuToggle?: (open: boolean) => void;
}
const contextMenuBelow = (elementRect: DOMRect) => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.pageXOffset + elementRect.width;
const top = elementRect.bottom + window.pageYOffset;
const chevronFace = ChevronFace.None;
return { left, top, chevronFace };
};
const ThreadListContextMenu: React.FC<IProps> = ({ mxEvent, permalinkCreator, onMenuToggle }) => {
const [optionsPosition, setOptionsPosition] = useState(null);
const closeThreadOptions = useCallback(() => {
setOptionsPosition(null);
}, []);
const viewInRoom = useCallback((evt: ButtonEvent): void => {
evt.preventDefault();
evt.stopPropagation();
dis.dispatch({
action: Action.ViewRoom,
event_id: mxEvent.getId(),
highlighted: true,
room_id: mxEvent.getRoomId(),
});
closeThreadOptions();
}, [mxEvent, closeThreadOptions]);
const copyLinkToThread = useCallback(async (evt: ButtonEvent) => {
evt.preventDefault();
evt.stopPropagation();
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
await copyPlaintext(matrixToUrl);
closeThreadOptions();
}, [mxEvent, closeThreadOptions, permalinkCreator]);
const toggleOptionsMenu = useCallback((ev: ButtonEvent): void => {
if (!!optionsPosition) {
closeThreadOptions();
} else {
const position = ev.currentTarget.getBoundingClientRect();
setOptionsPosition(position);
}
}, [closeThreadOptions, optionsPosition]);
useEffect(() => {
if (onMenuToggle) {
onMenuToggle(!!optionsPosition);
}
}, [optionsPosition, onMenuToggle]);
const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget(
MatrixClientPeg.get().getRoom(mxEvent.getRoomId()),
);
return <React.Fragment>
<ContextMenuTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
onClick={toggleOptionsMenu}
title={_t("Thread options")}
isExpanded={!!optionsPosition}
/>
{ !!optionsPosition && (<IconizedContextMenu
onFinished={closeThreadOptions}
className="mx_RoomTile_contextMenu"
compact
rightAligned
{...contextMenuBelow(optionsPosition)}
>
<IconizedContextMenuOptionList>
{ isMainSplitTimelineShown &&
<IconizedContextMenuOption
onClick={(e) => viewInRoom(e)}
label={_t("View in room")}
iconClassName="mx_ThreadPanel_viewInRoom"
/> }
<IconizedContextMenuOption
onClick={(e) => copyLinkToThread(e)}
label={_t("Copy link to thread")}
iconClassName="mx_ThreadPanel_copyLinkToThread"
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>) }
</React.Fragment>;
};
export default ThreadListContextMenu;