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:
Michael Telatynski 2021-11-30 18:08:46 +00:00 committed by GitHub
parent c09e0efdb9
commit 8fe582b094
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1433 additions and 1483 deletions

View file

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

View file

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

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

View file

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

View file

@ -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({