Tweaks to informational architecture 1.1 (#7052)
* Move user avatar to Space panel * Add room list header for 'Home' or 'Space Name' to room list Add existing Space context menus to room list header * Re-add pending room join spinner * Iterate RoomListHeader plus context menu * Iterate space context menu * Iterate room list + interactions * Move DND to new iA model * Replace composer custom status management with usermenu one * Cull Quick Actions * Iterate minimized room list state * delint * Merge the RoomListNumResults into the RoomListHeader * Make the search shortcut prompt semi-bold * Iterate RoomListHeader based on design review * Iterate UserMenu based on feedback * Add name to expanded spacepanel usermenu button * i18n * Make room sub list aux button components more generic * Change left panel explore button to only refer to room directory * Iterate RoomListHeader * Fix custom user status input field width in Chrome * Bring back Notification settings button * delint * i18n * post-merge fix * iterate pr * Remove unused state * update copy * Apply suggestions from PR review * delint * Update invite iconography * Iterate Space context menu to match Figma * Fix chevron alignment * Fix edge case for RoomListHeader on metaspaces * Wire up general rageshake-driven feedback mechanism * Add IA1.1 info toast * add missing alt attribute * delint * delint * tweak ia toast priority * e2e test account for new toast * autofocus feedback field and remove old subheading * tweak copy * Iterate space panel colours to match Figma * Iterate PR * delint * Fix feedback submission with object setting values * iterate based on review * Tweak colours and update splash image * Tweaks based on review * Remove room list prompt, made redundant by the big fat `+` * Fix edge cases around User Menu positioning and dnd * Add missing import, bad merge? * Update aria label in e2e test * Fix room list space rooms context menu explore button behaviour * Tweak copy * Revert order of options in the UserMenu * Tweak copy * i18n
This commit is contained in:
parent
c09e0efdb9
commit
8fe582b094
49 changed files with 1433 additions and 1483 deletions
|
@ -1,159 +0,0 @@
|
|||
/*
|
||||
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, { createRef } from 'react';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import classNames from 'classnames';
|
||||
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ContextMenu, { ChevronFace, ContextMenuButton } from "../../structures/ContextMenu";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
interface IProps {
|
||||
member: RoomMember;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: ResizeMethod;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
hasStatus: boolean;
|
||||
menuDisplayed: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
|
||||
export default class MemberStatusMessageAvatar extends React.Component<IProps, IState> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
private button = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasStatus: this.hasStatus,
|
||||
menuDisplayed: false,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
||||
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
||||
}
|
||||
if (!SettingsStore.getValue("feature_custom_status")) {
|
||||
return;
|
||||
}
|
||||
const { user } = this.props.member;
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
const { user } = this.props.member;
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
user.removeListener(
|
||||
"User._unstable_statusMessage",
|
||||
this.onStatusMessageCommitted,
|
||||
);
|
||||
}
|
||||
|
||||
private get hasStatus(): boolean {
|
||||
const { user } = this.props.member;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return !!user.unstable_statusMessage;
|
||||
}
|
||||
|
||||
private onStatusMessageCommitted = (): void => {
|
||||
// The `User` object has observed a status message change.
|
||||
this.setState({
|
||||
hasStatus: this.hasStatus,
|
||||
});
|
||||
};
|
||||
|
||||
private openMenu = (): void => {
|
||||
this.setState({ menuDisplayed: true });
|
||||
};
|
||||
|
||||
private closeMenu = (): void => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const avatar = <MemberAvatar
|
||||
member={this.props.member}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
resizeMethod={this.props.resizeMethod}
|
||||
/>;
|
||||
|
||||
if (!SettingsStore.getValue("feature_custom_status")) {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
"mx_MemberStatusMessageAvatar": true,
|
||||
"mx_MemberStatusMessageAvatar_hasStatus": this.state.hasStatus,
|
||||
});
|
||||
|
||||
let contextMenu;
|
||||
if (this.state.menuDisplayed) {
|
||||
const elementRect = this.button.current.getBoundingClientRect();
|
||||
|
||||
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
||||
const chevronMargin = 1; // Add some spacing away from target
|
||||
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronOffset={(elementRect.width - chevronWidth) / 2}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
left={elementRect.left + window.pageXOffset}
|
||||
top={elementRect.top + window.pageYOffset - chevronMargin}
|
||||
menuWidth={226}
|
||||
onFinished={this.closeMenu}
|
||||
>
|
||||
<StatusMessageContextMenu user={this.props.member.user} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<ContextMenuButton
|
||||
className={classes}
|
||||
inputRef={this.button}
|
||||
onClick={this.openMenu}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
label={_t("User Status")}
|
||||
>
|
||||
{ avatar }
|
||||
</ContextMenuButton>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import ContextMenu, {
|
|||
MenuItem,
|
||||
MenuItemCheckbox, MenuItemRadio,
|
||||
} from "../../structures/ContextMenu";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
interface IProps extends IContextMenuProps {
|
||||
className?: string;
|
||||
|
@ -41,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> {
|
||||
|
@ -73,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, {
|
||||
|
@ -85,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>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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,16 +94,17 @@ 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();
|
||||
|
@ -120,13 +118,13 @@ const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
|
|||
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,155 +0,0 @@
|
|||
/*
|
||||
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 { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
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>;
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
|
@ -26,6 +26,9 @@ import BugReportDialog from "./BugReportDialog";
|
|||
import InfoDialog from "./InfoDialog";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import { submitFeedback } from "../../../rageshake/submit-rageshake";
|
||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
|
||||
const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" +
|
||||
"?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
|
||||
|
@ -34,18 +37,33 @@ const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose"
|
|||
interface IProps extends IDialogProps {}
|
||||
|
||||
const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
||||
const feedbackRef = useRef<Field>();
|
||||
const [rating, setRating] = useState<Rating>();
|
||||
const [comment, setComment] = useState<string>("");
|
||||
const [canContact, toggleCanContact] = useStateToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
// autofocus doesn't work on textareas
|
||||
feedbackRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const onDebugLogsLinkClick = (): void => {
|
||||
props.onFinished();
|
||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||
};
|
||||
|
||||
const hasFeedback = CountlyAnalytics.instance.canEnable();
|
||||
const countlyEnabled = CountlyAnalytics.instance.canEnable();
|
||||
const rageshakeUrl = SdkConfig.get().bug_report_endpoint_url;
|
||||
|
||||
const hasFeedback = countlyEnabled || rageshakeUrl;
|
||||
const onFinished = (sendFeedback: boolean): void => {
|
||||
if (hasFeedback && sendFeedback) {
|
||||
CountlyAnalytics.instance.reportFeedback(rating, comment);
|
||||
if (rageshakeUrl) {
|
||||
submitFeedback(rageshakeUrl, "feedback", comment, canContact);
|
||||
} else if (countlyEnabled) {
|
||||
CountlyAnalytics.instance.reportFeedback(rating, comment);
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog('Feedback sent', '', InfoDialog, {
|
||||
title: _t('Feedback sent'),
|
||||
description: _t('Thank you!'),
|
||||
|
@ -56,56 +74,73 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
|||
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
let countlyFeedbackSection;
|
||||
if (hasFeedback) {
|
||||
countlyFeedbackSection = <React.Fragment>
|
||||
<hr />
|
||||
<div className="mx_FeedbackDialog_section mx_FeedbackDialog_rateApp">
|
||||
<h3>{ _t("Rate %(brand)s", { brand }) }</h3>
|
||||
let feedbackSection;
|
||||
if (rageshakeUrl) {
|
||||
feedbackSection = <div className="mx_FeedbackDialog_section mx_FeedbackDialog_rateApp">
|
||||
<h3>{ _t("Comment") }</h3>
|
||||
|
||||
<p>{ _t("Tell us below how you feel about %(brand)s so far.", { brand }) }</p>
|
||||
<p>{ _t("Please go into as much detail as you like, so we can track down the problem.") }</p>
|
||||
<p>{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }</p>
|
||||
|
||||
<StyledRadioGroup
|
||||
name="feedbackRating"
|
||||
value={String(rating)}
|
||||
onChange={(r) => setRating(parseInt(r, 10) as Rating)}
|
||||
definitions={[
|
||||
{ value: "1", label: "😠" },
|
||||
{ value: "2", label: "😞" },
|
||||
{ value: "3", label: "😑" },
|
||||
{ value: "4", label: "😄" },
|
||||
{ value: "5", label: "😍" },
|
||||
]}
|
||||
/>
|
||||
<Field
|
||||
id="feedbackComment"
|
||||
label={_t("Feedback")}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={comment}
|
||||
element="textarea"
|
||||
onChange={(ev) => {
|
||||
setComment(ev.target.value);
|
||||
}}
|
||||
ref={feedbackRef}
|
||||
/>
|
||||
|
||||
<Field
|
||||
id="feedbackComment"
|
||||
label={_t("Add comment")}
|
||||
placeholder={_t("Comment")}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={comment}
|
||||
element="textarea"
|
||||
onChange={(ev) => {
|
||||
setComment(ev.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
<StyledCheckbox
|
||||
checked={canContact}
|
||||
onChange={toggleCanContact}
|
||||
>
|
||||
{ _t("You may contact me if you want to follow up or to let me test out upcoming ideas") }
|
||||
</StyledCheckbox>
|
||||
</div>;
|
||||
} else if (countlyEnabled) {
|
||||
feedbackSection = <div className="mx_FeedbackDialog_section mx_FeedbackDialog_rateApp">
|
||||
<h3>{ _t("Rate %(brand)s", { brand }) }</h3>
|
||||
|
||||
let subheading;
|
||||
if (hasFeedback) {
|
||||
subheading = (
|
||||
<h2>{ _t("There are two ways you can provide feedback and help us improve %(brand)s.", { brand }) }</h2>
|
||||
);
|
||||
<p>{ _t("Tell us below how you feel about %(brand)s so far.", { brand }) }</p>
|
||||
<p>{ _t("Please go into as much detail as you like, so we can track down the problem.") }</p>
|
||||
|
||||
<StyledRadioGroup
|
||||
name="feedbackRating"
|
||||
value={String(rating)}
|
||||
onChange={(r) => setRating(parseInt(r, 10) as Rating)}
|
||||
definitions={[
|
||||
{ value: "1", label: "😠" },
|
||||
{ value: "2", label: "😞" },
|
||||
{ value: "3", label: "😑" },
|
||||
{ value: "4", label: "😄" },
|
||||
{ value: "5", label: "😍" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Field
|
||||
id="feedbackComment"
|
||||
label={_t("Add comment")}
|
||||
placeholder={_t("Comment")}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={comment}
|
||||
element="textarea"
|
||||
onChange={(ev) => {
|
||||
setComment(ev.target.value);
|
||||
}}
|
||||
ref={feedbackRef}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let bugReports = null;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
if (rageshakeUrl) {
|
||||
bugReports = (
|
||||
<p>{
|
||||
<p className="mx_FeedbackDialog_section_microcopy">{
|
||||
_t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
|
||||
"to help us track down the problem.", {}, {
|
||||
debugLogsLink: sub => (
|
||||
|
@ -121,8 +156,6 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
|||
hasCancelButton={!!hasFeedback}
|
||||
title={_t("Feedback")}
|
||||
description={<React.Fragment>
|
||||
{ subheading }
|
||||
|
||||
<div className="mx_FeedbackDialog_section mx_FeedbackDialog_reportBug">
|
||||
<h3>{ _t("Report a bug") }</h3>
|
||||
<p>{
|
||||
|
@ -138,10 +171,10 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
|||
}</p>
|
||||
{ bugReports }
|
||||
</div>
|
||||
{ countlyFeedbackSection }
|
||||
{ feedbackSection }
|
||||
</React.Fragment>}
|
||||
button={hasFeedback ? _t("Send feedback") : _t("Go back")}
|
||||
buttonDisabled={hasFeedback && !rating}
|
||||
buttonDisabled={hasFeedback && !rageshakeUrl && !rating}
|
||||
onFinished={onFinished}
|
||||
/>);
|
||||
};
|
||||
|
|
|
@ -49,7 +49,6 @@ import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInse
|
|||
import { Action } from "../../../dispatcher/actions";
|
||||
import EditorModel from "../../../editor/model";
|
||||
import EmojiPicker from '../emojipicker/EmojiPicker';
|
||||
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
|
||||
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
||||
import Modal from "../../../Modal";
|
||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
@ -61,16 +60,6 @@ import PollCreateDialog from "../elements/PollCreateDialog";
|
|||
let instanceCount = 0;
|
||||
const NARROW_MODE_BREAKPOINT = 500;
|
||||
|
||||
interface IComposerAvatarProps {
|
||||
me: RoomMember;
|
||||
}
|
||||
|
||||
function ComposerAvatar(props: IComposerAvatarProps) {
|
||||
return <div className="mx_MessageComposer_avatar">
|
||||
<MemberStatusMessageAvatar member={props.me} width={24} height={24} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
interface ISendButtonProps {
|
||||
onClick: () => void;
|
||||
title?: string; // defaults to something generic
|
||||
|
@ -567,7 +556,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
render() {
|
||||
const controls = [
|
||||
this.state.me && !this.props.compact ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
||||
this.props.e2eStatus ?
|
||||
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
|
||||
null,
|
||||
|
|
|
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, ReactComponentElement } from "react";
|
||||
import { Dispatcher } from "flux";
|
||||
import React, { ComponentType, createRef, ReactComponentElement, RefObject } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
@ -27,9 +26,8 @@ import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/Roo
|
|||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import { ITagMap } from "../../../stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, isCustomTag, TagID } from "../../../stores/room-list/models";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import RoomSublist from "./RoomSublist";
|
||||
import RoomSublist, { IAuxButtonProps } from "./RoomSublist";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import GroupAvatar from "../avatars/GroupAvatar";
|
||||
|
@ -41,18 +39,28 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo
|
|||
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
|
||||
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
|
||||
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
||||
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { ISuggestedRoom, MetaSpace, SpaceKey, UPDATE_SUGGESTED_ROOMS } from "../../../stores/spaces";
|
||||
import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
|
||||
import {
|
||||
ISuggestedRoom,
|
||||
MetaSpace,
|
||||
SpaceKey,
|
||||
UPDATE_SUGGESTED_ROOMS,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
} from "../../../stores/spaces";
|
||||
import { shouldShowSpaceInvite, showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
|
@ -95,9 +103,7 @@ const ALWAYS_VISIBLE_TAGS: TagID[] = [
|
|||
interface ITagAesthetics {
|
||||
sectionLabel: string;
|
||||
sectionLabelRaw?: string;
|
||||
addRoomLabel?: string;
|
||||
onAddRoom?: (dispatcher?: Dispatcher<ActionPayload>) => void;
|
||||
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
|
||||
AuxButtonComponent?: ComponentType<IAuxButtonProps>;
|
||||
isInvite: boolean;
|
||||
defaultHidden: boolean;
|
||||
}
|
||||
|
@ -107,6 +113,194 @@ interface ITagAestheticsMap {
|
|||
[tagId: TagID]: ITagAesthetics;
|
||||
}
|
||||
|
||||
const auxButtonContextMenuPosition = (handle: RefObject<HTMLDivElement>) => {
|
||||
const rect = handle.current.getBoundingClientRect();
|
||||
return {
|
||||
chevronFace: ChevronFace.None,
|
||||
left: rect.left - 7,
|
||||
top: rect.top + rect.height,
|
||||
};
|
||||
};
|
||||
|
||||
const DmAuxButton = ({ tabIndex, dispatcher = defaultDispatcher }: IAuxButtonProps) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
|
||||
return SpaceStore.instance.activeSpaceRoom;
|
||||
});
|
||||
|
||||
const showCreateRooms = shouldShowComponent(UIComponent.CreateRooms);
|
||||
const showInviteUsers = shouldShowComponent(UIComponent.InviteUsers);
|
||||
|
||||
if (activeSpace && (showCreateRooms || showInviteUsers)) {
|
||||
let contextMenu: JSX.Element;
|
||||
if (menuDisplayed) {
|
||||
const canInvite = shouldShowSpaceInvite(activeSpace);
|
||||
|
||||
contextMenu = <IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ showCreateRooms && <IconizedContextMenuOption
|
||||
label={_t("Start new chat")}
|
||||
iconClassName="mx_RoomList_iconStartChat"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({ action: "view_create_chat" });
|
||||
}}
|
||||
/> }
|
||||
{ showInviteUsers && <IconizedContextMenuOption
|
||||
label={_t("Invite to space")}
|
||||
iconClassName="mx_RoomList_iconInvite"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
showSpaceInvite(activeSpace);
|
||||
}}
|
||||
disabled={!canInvite}
|
||||
tooltip={canInvite ? undefined
|
||||
: _t("You do not have permissions to invite people to this space")}
|
||||
/> }
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<ContextMenuTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={openMenu}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={_t("Add people")}
|
||||
title={_t("Add people")}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={handle}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</>;
|
||||
} else if (!activeSpace && showCreateRooms) {
|
||||
return <AccessibleTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={() => dispatcher.dispatch({ action: 'view_create_chat' })}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={_t("Start chat")}
|
||||
title={_t("Start chat")}
|
||||
/>;
|
||||
}
|
||||
};
|
||||
|
||||
const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
const activeSpace = useEventEmitterState<Room>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
|
||||
return SpaceStore.instance.activeSpaceRoom;
|
||||
});
|
||||
|
||||
const showCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
|
||||
|
||||
let contextMenuContent: JSX.Element;
|
||||
if (menuDisplayed && activeSpace) {
|
||||
const canAddRooms = activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
|
||||
MatrixClientPeg.get().getUserId());
|
||||
|
||||
contextMenuContent = <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: activeSpace.roomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{
|
||||
showCreateRoom
|
||||
? (<>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconCreateNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
showCreateNewRoom(activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconAddExistingRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
showAddExistingRooms(activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to add rooms to this space")}
|
||||
/>
|
||||
</>)
|
||||
: null
|
||||
}
|
||||
</IconizedContextMenuOptionList>;
|
||||
} else if (menuDisplayed) {
|
||||
contextMenuContent = <IconizedContextMenuOptionList first>
|
||||
{ showCreateRoom && <IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconCreateNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
}}
|
||||
/> }
|
||||
<IconizedContextMenuOption
|
||||
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
|
||||
? _t("Explore community rooms")
|
||||
: _t("Explore public rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
}
|
||||
|
||||
let contextMenu: JSX.Element;
|
||||
if (menuDisplayed) {
|
||||
contextMenu = <IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
|
||||
{ contextMenuContent }
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<ContextMenuTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={openMenu}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={_td("Add room")}
|
||||
title={_td("Add room")}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={handle}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</>;
|
||||
};
|
||||
|
||||
const TAG_AESTHETICS: ITagAestheticsMap = {
|
||||
[DefaultTagID.Invite]: {
|
||||
sectionLabel: _td("Invites"),
|
||||
|
@ -122,93 +316,13 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
sectionLabel: _td("People"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Start chat"),
|
||||
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
|
||||
(dispatcher || defaultDispatcher).dispatch({ action: 'view_create_chat' });
|
||||
},
|
||||
AuxButtonComponent: DmAuxButton,
|
||||
},
|
||||
[DefaultTagID.Untagged]: {
|
||||
sectionLabel: _td("Rooms"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Add room"),
|
||||
addRoomContextMenu: (onFinished: () => void) => {
|
||||
if (SpaceStore.instance.activeSpaceRoom) {
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const space = SpaceStore.instance.activeSpaceRoom;
|
||||
const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
|
||||
|
||||
return <IconizedContextMenuOptionList first>
|
||||
{
|
||||
shouldShowComponent(UIComponent.CreateRooms)
|
||||
? (<>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showCreateNewRoom(space);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconHash"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showAddExistingRooms(space);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to add rooms to this space")}
|
||||
/>
|
||||
</>)
|
||||
: null
|
||||
}
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomList_iconBrowse"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
}
|
||||
|
||||
return <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
|
||||
? _t("Explore community rooms")
|
||||
: _t("Explore public rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
},
|
||||
AuxButtonComponent: UntaggedAuxButton,
|
||||
},
|
||||
[DefaultTagID.LowPriority]: {
|
||||
sectionLabel: _td("Low priority"),
|
||||
|
@ -296,7 +410,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
const currentRoomId = RoomViewStore.getRoomId();
|
||||
const room = this.getRoomDelta(currentRoomId, viewRoomDeltaPayload.delta, viewRoomDeltaPayload.unread);
|
||||
if (room) {
|
||||
dis.dispatch({
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
show_room_tile: true, // to make sure the room gets scrolled into view
|
||||
|
@ -375,17 +489,19 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private onStartChat = () => {
|
||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||
dis.dispatch({ action: "view_create_chat", initialText });
|
||||
defaultDispatcher.dispatch({ action: "view_create_chat", initialText });
|
||||
};
|
||||
|
||||
private onExplore = () => {
|
||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
||||
};
|
||||
|
||||
private onSpaceInviteClick = () => {
|
||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||
showSpaceInvite(this.context.getRoom(this.props.activeSpace), initialText);
|
||||
if (this.props.activeSpace[0] === "!") {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: SpaceStore.instance.activeSpace,
|
||||
});
|
||||
} else {
|
||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
||||
}
|
||||
};
|
||||
|
||||
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
|
||||
|
@ -508,9 +624,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
forRooms={true}
|
||||
startAsHidden={aesthetics.defaultHidden}
|
||||
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
|
||||
onAddRoom={aesthetics.onAddRoom}
|
||||
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
|
||||
addRoomContextMenu={aesthetics.addRoomContextMenu}
|
||||
AuxButtonComponent={aesthetics.AuxButtonComponent}
|
||||
isMinimized={this.props.isMinimized}
|
||||
showSkeleton={showSkeleton}
|
||||
extraTiles={extraTiles}
|
||||
|
@ -528,10 +642,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const userId = cli.getUserId();
|
||||
const activeSpace = this.props.activeSpace[0] === "!" ? cli.getRoom(this.props.activeSpace) : null;
|
||||
|
||||
let explorePrompt: JSX.Element;
|
||||
if (!this.props.isMinimized) {
|
||||
if (this.state.isNameFiltering) {
|
||||
|
@ -549,58 +659,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
kind="link"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{ activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
|
||||
{ this.props.activeSpace[0] === "!" ? _t("Explore rooms") : _t("Explore all public rooms") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else if (
|
||||
activeSpace?.canInvite(userId) ||
|
||||
activeSpace?.getMyMembership() === "join" ||
|
||||
activeSpace?.getJoinRule() === JoinRule.Public
|
||||
) {
|
||||
const spaceName = activeSpace.name;
|
||||
const canInvite = activeSpace?.canInvite(userId) || activeSpace?.getJoinRule() === JoinRule.Public;
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{ _t("Quick actions") }</div>
|
||||
{ canInvite && <AccessibleTooltipButton
|
||||
className="mx_RoomList_explorePrompt_spaceInvite"
|
||||
onClick={this.onSpaceInviteClick}
|
||||
title={_t("Invite to %(spaceName)s", { spaceName })}
|
||||
>
|
||||
{ _t("Invite people") }
|
||||
</AccessibleTooltipButton> }
|
||||
{ activeSpace?.getMyMembership() === "join" && <AccessibleTooltipButton
|
||||
className="mx_RoomList_explorePrompt_spaceExplore"
|
||||
onClick={this.onExplore}
|
||||
title={_t("Explore %(spaceName)s", { spaceName })}
|
||||
>
|
||||
{ _t("Explore rooms") }
|
||||
</AccessibleTooltipButton> }
|
||||
</div>;
|
||||
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
||||
const unfilteredLists = RoomListStore.instance.unfilteredLists;
|
||||
const unfilteredRooms = unfilteredLists[DefaultTagID.Untagged] || [];
|
||||
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
||||
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
|
||||
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical.length < 1 && unfilteredFavourite.length < 1) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_startChat"
|
||||
kind="link"
|
||||
onClick={this.onStartChat}
|
||||
>
|
||||
{ _t("Start a new chat") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_explore"
|
||||
kind="link"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{ _t("Explore all public rooms") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
367
src/components/views/rooms/RoomListHeader.tsx
Normal file
367
src/components/views/rooms/RoomListHeader.tsx
Normal file
|
@ -0,0 +1,367 @@
|
|||
/*
|
||||
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, useEffect, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
|
||||
import { HomeButtonContextMenu } from "../spaces/SpacePanel";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { shouldShowSpaceInvite, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import EditCommunityPrototypeDialog from "../dialogs/EditCommunityPrototypeDialog";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import { showCommunityInviteDialog } from "../../../RoomInvite";
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import InlineSpinner from "../elements/InlineSpinner";
|
||||
import TooltipButton from "../elements/TooltipButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import {
|
||||
getMetaSpaceName,
|
||||
MetaSpace,
|
||||
SpaceKey,
|
||||
UPDATE_HOME_BEHAVIOUR,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
} from "../../../stores/spaces";
|
||||
|
||||
const contextMenuBelow = (elementRect: DOMRect) => {
|
||||
// align the context menu's icons with the icon which opened the context menu
|
||||
const left = elementRect.left + window.pageXOffset;
|
||||
const top = elementRect.bottom + window.pageYOffset + 12;
|
||||
const chevronFace = ChevronFace.None;
|
||||
return { left, top, chevronFace };
|
||||
};
|
||||
|
||||
const PrototypeCommunityContextMenu = (props) => {
|
||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
|
||||
let settingsOption;
|
||||
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
|
||||
const onCommunitySettingsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
|
||||
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
|
||||
});
|
||||
props.onFinished();
|
||||
};
|
||||
|
||||
settingsOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
aria-label={_t("Community settings")}
|
||||
onClick={onCommunitySettingsClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const onCommunityMembersClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// We'd ideally just pop open a right panel with the member list, but the current
|
||||
// way the right panel is structured makes this exceedingly difficult. Instead, we'll
|
||||
// switch to the general room and open the member list there as it should be in sync
|
||||
// anyways.
|
||||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||
if (chat) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: chat.roomId,
|
||||
}, true);
|
||||
dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList });
|
||||
} else {
|
||||
// "This should never happen" clauses go here for the prototype.
|
||||
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
|
||||
title: _t('Failed to find the general chat for this community'),
|
||||
description: _t("Failed to find the general chat for this community"),
|
||||
});
|
||||
}
|
||||
props.onFinished();
|
||||
};
|
||||
|
||||
return <IconizedContextMenu {...props} compact>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ settingsOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMembers"
|
||||
label={_t("Members")}
|
||||
onClick={onCommunityMembersClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
};
|
||||
|
||||
const useJoiningRooms = (): Set<string> => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [joiningRooms, setJoiningRooms] = useState(new Set<string>());
|
||||
useDispatcher(defaultDispatcher, payload => {
|
||||
switch (payload.action) {
|
||||
case Action.JoinRoom:
|
||||
setJoiningRooms(new Set(joiningRooms.add(payload.roomId)));
|
||||
break;
|
||||
case Action.JoinRoomReady:
|
||||
case Action.JoinRoomError:
|
||||
if (joiningRooms.delete(payload.roomId)) {
|
||||
setJoiningRooms(new Set(joiningRooms));
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
useEventEmitter(cli, "Room", (room: Room) => {
|
||||
if (joiningRooms.delete(room.roomId)) {
|
||||
setJoiningRooms(new Set(joiningRooms));
|
||||
}
|
||||
});
|
||||
|
||||
return joiningRooms;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
onVisibilityChange?(): void;
|
||||
}
|
||||
|
||||
const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [mainMenuDisplayed, mainMenuHandle, openMainMenu, closeMainMenu] = useContextMenu<HTMLDivElement>();
|
||||
const [plusMenuDisplayed, plusMenuHandle, openPlusMenu, closePlusMenu] = useContextMenu<HTMLDivElement>();
|
||||
const [spaceKey, activeSpace] = useEventEmitterState<[SpaceKey, Room | null]>(
|
||||
SpaceStore.instance,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
() => [SpaceStore.instance.activeSpace, SpaceStore.instance.activeSpaceRoom],
|
||||
);
|
||||
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
|
||||
return SpaceStore.instance.allRoomsInHome;
|
||||
});
|
||||
const joiningRooms = useJoiningRooms();
|
||||
|
||||
const count = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
|
||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
||||
return Object.values(RoomListStore.instance.orderedLists).flat(1).length;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (onVisibilityChange) {
|
||||
onVisibilityChange();
|
||||
}
|
||||
}, [count, onVisibilityChange]);
|
||||
|
||||
if (typeof count === "number") {
|
||||
return <div className="mx_LeftPanel_roomListFilterCount">
|
||||
{ _t("%(count)s results", { count }) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
|
||||
let contextMenu: JSX.Element;
|
||||
if (mainMenuDisplayed) {
|
||||
let ContextMenuComponent;
|
||||
if (activeSpace) {
|
||||
ContextMenuComponent = SpaceContextMenu;
|
||||
} else if (communityId) {
|
||||
ContextMenuComponent = PrototypeCommunityContextMenu;
|
||||
} else {
|
||||
ContextMenuComponent = HomeButtonContextMenu;
|
||||
}
|
||||
|
||||
contextMenu = <ContextMenuComponent
|
||||
{...contextMenuBelow(mainMenuHandle.current.getBoundingClientRect())}
|
||||
space={activeSpace}
|
||||
onFinished={closeMainMenu}
|
||||
hideHeader={true}
|
||||
/>;
|
||||
} else if (plusMenuDisplayed && activeSpace) {
|
||||
let inviteOption: JSX.Element;
|
||||
if (shouldShowSpaceInvite(activeSpace)) {
|
||||
inviteOption = <IconizedContextMenuOption
|
||||
label={_t("Invite")}
|
||||
iconClassName="mx_RoomListHeader_iconInvite"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showSpaceInvite(activeSpace);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>;
|
||||
} else if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
|
||||
inviteOption = <IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconInvite"
|
||||
label={_t("Invite")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
let createNewRoomOption: JSX.Element;
|
||||
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId())) {
|
||||
createNewRoomOption = <IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconCreateRoom"
|
||||
label={_t("Create new room")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewRoom(activeSpace);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
|
||||
onFinished={closePlusMenu}
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ inviteOption }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Start new chat")}
|
||||
iconClassName="mx_RoomListHeader_iconStartChat"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({ action: "view_create_chat" });
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
{ createNewRoomOption }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomListHeader_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory });
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
} else if (plusMenuDisplayed) {
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
|
||||
onFinished={closePlusMenu}
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Start new chat")}
|
||||
iconClassName="mx_RoomListHeader_iconStartChat"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({ action: "view_create_chat" });
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomListHeader_iconCreateRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Join public room")}
|
||||
iconClassName="mx_RoomListHeader_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory });
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
let title: string;
|
||||
if (activeSpace) {
|
||||
title = activeSpace.name;
|
||||
} else if (communityId) {
|
||||
title = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
} else {
|
||||
title = getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome);
|
||||
}
|
||||
|
||||
let pendingRoomJoinSpinner;
|
||||
if (joiningRooms.size) {
|
||||
pendingRoomJoinSpinner = <InlineSpinner>
|
||||
<TooltipButton helpText={_t(
|
||||
"Currently joining %(count)s rooms",
|
||||
{ count: joiningRooms.size },
|
||||
)} />
|
||||
</InlineSpinner>;
|
||||
}
|
||||
|
||||
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{ title }</div>;
|
||||
if (activeSpace || spaceKey === MetaSpace.Home) {
|
||||
contextMenuButton = <ContextMenuTooltipButton
|
||||
inputRef={mainMenuHandle}
|
||||
onClick={openMainMenu}
|
||||
isExpanded={mainMenuDisplayed}
|
||||
className="mx_RoomListHeader_contextMenuButton"
|
||||
title={activeSpace
|
||||
? _t("%(spaceName)s menu", { spaceName: activeSpace.name })
|
||||
: _t("Home options")}
|
||||
>
|
||||
{ title }
|
||||
</ContextMenuTooltipButton>;
|
||||
}
|
||||
|
||||
return <div className="mx_RoomListHeader">
|
||||
{ contextMenuButton }
|
||||
{ pendingRoomJoinSpinner }
|
||||
<ContextMenuTooltipButton
|
||||
inputRef={plusMenuHandle}
|
||||
onClick={openPlusMenu}
|
||||
isExpanded={plusMenuDisplayed}
|
||||
className="mx_RoomListHeader_plusButton"
|
||||
title={_t("Add")}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default RoomListHeader;
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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, { useEffect, useState } from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
|
||||
interface IProps {
|
||||
onVisibilityChange?: () => void;
|
||||
}
|
||||
|
||||
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
const [count, setCount] = useState<number>(null);
|
||||
useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
|
||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
||||
const numRooms = Object.values(RoomListStore.instance.orderedLists).flat(1).length;
|
||||
setCount(numRooms);
|
||||
} else {
|
||||
setCount(null);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (onVisibilityChange) {
|
||||
onVisibilityChange();
|
||||
}
|
||||
}, [count, onVisibilityChange]);
|
||||
|
||||
if (typeof count !== "number") return null;
|
||||
|
||||
return <div className="mx_LeftPanel_roomListFilterCount">
|
||||
{ SpaceStore.instance.spacePanelSpaces.length
|
||||
? _t("%(count)s results in all spaces", { count })
|
||||
: _t("%(count)s results", { count })
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default RoomListNumResults;
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { createRef, ReactComponentElement } from "react";
|
||||
import { ComponentType, createRef, ReactComponentElement } from "react";
|
||||
import { normalize } from "matrix-js-sdk/src/utils";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from 'classnames';
|
||||
|
@ -52,11 +52,9 @@ import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
|
|||
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
|
||||
import ExtraTile from "./ExtraTile";
|
||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { Dispatcher } from "flux";
|
||||
|
||||
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
|
||||
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
||||
|
@ -67,13 +65,16 @@ const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
|
|||
// HACK: We really shouldn't have to do this.
|
||||
polyfillTouchEvent();
|
||||
|
||||
export interface IAuxButtonProps {
|
||||
tabIndex: number;
|
||||
dispatcher?: Dispatcher<ActionPayload>;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
forRooms: boolean;
|
||||
startAsHidden: boolean;
|
||||
label: string;
|
||||
onAddRoom?: () => void;
|
||||
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
|
||||
addRoomLabel: string;
|
||||
AuxButtonComponent?: ComponentType<IAuxButtonProps>;
|
||||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
showSkeleton?: boolean;
|
||||
|
@ -81,8 +82,6 @@ interface IProps {
|
|||
resizeNotifier: ResizeNotifier;
|
||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||
onListCollapse?: (isExpanded: boolean) => void;
|
||||
|
||||
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
|
||||
}
|
||||
|
||||
// TODO: Use re-resizer's NumberSize when it is exposed as the type
|
||||
|
@ -95,7 +94,6 @@ type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
|
|||
|
||||
interface IState {
|
||||
contextMenuPosition: PartialDOMRect;
|
||||
addRoomContextMenuPosition: PartialDOMRect;
|
||||
isResizing: boolean;
|
||||
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
||||
height: number;
|
||||
|
@ -123,7 +121,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
|
||||
this.state = {
|
||||
contextMenuPosition: null,
|
||||
addRoomContextMenuPosition: null,
|
||||
isResizing: false,
|
||||
isExpanded: this.isBeingFiltered ? this.isBeingFiltered : !this.layout.isCollapsed,
|
||||
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
|
||||
|
@ -313,11 +310,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onAddRoom = (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.props.onAddRoom) this.props.onAddRoom();
|
||||
};
|
||||
|
||||
private applyHeightChange(newHeight: number) {
|
||||
const heightInTiles = Math.ceil(this.layout.pixelsToTiles(newHeight - this.padding));
|
||||
this.layout.visibleTiles = Math.min(this.numTiles, heightInTiles);
|
||||
|
@ -395,21 +387,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onAddRoomContextMenu = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
this.setState({ addRoomContextMenuPosition: target.getBoundingClientRect() });
|
||||
};
|
||||
|
||||
private onCloseMenu = () => {
|
||||
this.setState({ contextMenuPosition: null });
|
||||
};
|
||||
|
||||
private onCloseAddRoomMenu = () => {
|
||||
this.setState({ addRoomContextMenuPosition: null });
|
||||
};
|
||||
|
||||
private onUnreadFirstChanged = () => {
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
|
||||
|
@ -627,18 +608,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
} else if (this.state.addRoomContextMenuPosition) {
|
||||
contextMenu = (
|
||||
<IconizedContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
left={this.state.addRoomContextMenuPosition.left - 7} // center align with the handle
|
||||
top={this.state.addRoomContextMenuPosition.top + this.state.addRoomContextMenuPosition.height}
|
||||
onFinished={this.onCloseAddRoomMenu}
|
||||
compact
|
||||
>
|
||||
{ this.props.addRoomContextMenu(this.onCloseAddRoomMenu) }
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -677,30 +646,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
);
|
||||
|
||||
let addRoomButton = null;
|
||||
if (!!this.props.onAddRoom && shouldShowComponent(UIComponent.CreateRooms)) {
|
||||
addRoomButton = (
|
||||
<AccessibleTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={this.onAddRoom}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={this.props.addRoomLabel || _t("Add room")}
|
||||
title={this.props.addRoomLabel}
|
||||
/>
|
||||
);
|
||||
} else if (this.props.addRoomContextMenu) {
|
||||
// We assume that shouldShowComponent() is checked by the context menu itself.
|
||||
addRoomButton = (
|
||||
<ContextMenuTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={this.onAddRoomContextMenu}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={this.props.addRoomLabel || _t("Add room")}
|
||||
title={this.props.addRoomLabel}
|
||||
isExpanded={!!this.state.addRoomContextMenuPosition}
|
||||
/>
|
||||
);
|
||||
if (this.props.AuxButtonComponent) {
|
||||
const AuxButtonComponent = this.props.AuxButtonComponent;
|
||||
addRoomButton = <AuxButtonComponent tabIndex={tabIndex} />;
|
||||
}
|
||||
|
||||
const collapseClasses = classNames({
|
||||
|
|
|
@ -36,6 +36,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
|||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import {
|
||||
getMetaSpaceName,
|
||||
MetaSpace,
|
||||
SpaceKey,
|
||||
UPDATE_HOME_BEHAVIOUR,
|
||||
|
@ -56,6 +57,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
|
|||
import UIStore from "../../../stores/UIStore";
|
||||
import QuickSettingsButton from "./QuickSettingsButton";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import UserMenu from "../../structures/UserMenu";
|
||||
|
||||
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
|
||||
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
|
||||
|
@ -80,7 +82,11 @@ interface IInnerSpacePanelProps {
|
|||
setPanelCollapsed: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps<typeof SpaceContextMenu>) => {
|
||||
export const HomeButtonContextMenu = ({
|
||||
onFinished,
|
||||
hideHeader,
|
||||
...props
|
||||
}: ComponentProps<typeof SpaceContextMenu>) => {
|
||||
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
|
||||
return SpaceStore.instance.allRoomsInHome;
|
||||
});
|
||||
|
@ -91,9 +97,9 @@ const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps<typeof S
|
|||
className="mx_SpacePanel_contextMenu"
|
||||
compact
|
||||
>
|
||||
<div className="mx_SpacePanel_contextMenu_header">
|
||||
{ !hideHeader && <div className="mx_SpacePanel_contextMenu_header">
|
||||
{ _t("Home") }
|
||||
</div>
|
||||
</div> }
|
||||
<IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuCheckbox
|
||||
iconClassName="mx_SpacePanel_noIcon"
|
||||
|
@ -135,7 +141,7 @@ const HomeButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
|
|||
className="mx_SpaceButton_home"
|
||||
selected={selected}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
label={allRoomsInHome ? _t("All rooms") : _t("Home")}
|
||||
label={getMetaSpaceName(MetaSpace.Home, allRoomsInHome)}
|
||||
notificationState={allRoomsInHome
|
||||
? RoomNotificationStateStore.instance.globalState
|
||||
: SpaceStore.instance.getNotificationState(MetaSpace.Home)}
|
||||
|
@ -150,7 +156,7 @@ const FavouritesButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps)
|
|||
className="mx_SpaceButton_favourites"
|
||||
selected={selected}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
label={_t("Favourites")}
|
||||
label={getMetaSpaceName(MetaSpace.Favourites)}
|
||||
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.Favourites)}
|
||||
/>;
|
||||
};
|
||||
|
@ -161,7 +167,7 @@ const PeopleButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) => {
|
|||
className="mx_SpaceButton_people"
|
||||
selected={selected}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
label={_t("People")}
|
||||
label={getMetaSpaceName(MetaSpace.People)}
|
||||
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.People)}
|
||||
/>;
|
||||
};
|
||||
|
@ -172,7 +178,7 @@ const OrphansButton = ({ selected, isPanelCollapsed }: MetaSpaceButtonProps) =>
|
|||
className="mx_SpaceButton_orphans"
|
||||
selected={selected}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
label={_t("Other rooms")}
|
||||
label={getMetaSpaceName(MetaSpace.Orphans)}
|
||||
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.Orphans)}
|
||||
/>;
|
||||
};
|
||||
|
@ -246,6 +252,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
|
|||
});
|
||||
|
||||
return <div className="mx_SpaceTreeLevel">
|
||||
<UserMenu isPanelCollapsed={isPanelCollapsed} />
|
||||
{ metaSpacesSection }
|
||||
{ invites.map(s => (
|
||||
<SpaceItem
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue