Merge branch 'develop' into sort-imports
Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
commit
7b94e13a84
642 changed files with 30052 additions and 8035 deletions
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
313
src/components/views/context_menus/RoomContextMenu.tsx
Normal file
313
src/components/views/context_menus/RoomContextMenu.tsx
Normal 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;
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
119
src/components/views/context_menus/ThreadListContextMenu.tsx
Normal file
119
src/components/views/context_menus/ThreadListContextMenu.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue