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
|
@ -24,7 +24,6 @@ import RoomList from "../views/rooms/RoomList";
|
|||
import CallHandler from "../../CallHandler";
|
||||
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import UserMenu from "./UserMenu";
|
||||
import RoomSearch from "./RoomSearch";
|
||||
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
|
||||
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
||||
|
@ -33,15 +32,14 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
|
|||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
|
||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||
import LeftPanelWidget from "./LeftPanelWidget";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomListHeader from "../views/rooms/RoomListHeader";
|
||||
import { Key } from "../../Keyboard";
|
||||
|
||||
interface IProps {
|
||||
|
@ -63,9 +61,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
private focusedElement = null;
|
||||
private isDoingStickyHeaders = false;
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
|
@ -327,14 +322,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private renderHeader(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_LeftPanel_userHeader">
|
||||
<UserMenu isMinimized={this.props.isMinimized} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBreadcrumbs(): React.ReactNode {
|
||||
if (this.state.showBreadcrumbs && !this.props.isMinimized) {
|
||||
return (
|
||||
|
@ -362,7 +349,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
/>;
|
||||
}
|
||||
|
||||
const space = this.state.activeSpace[0] === "!" ? this.context.getRoom(this.state.activeSpace) : null;
|
||||
return (
|
||||
<div
|
||||
className="mx_LeftPanel_filterContainer"
|
||||
|
@ -378,13 +364,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
|
||||
{ dialPadButton }
|
||||
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_LeftPanel_exploreButton", {
|
||||
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
|
||||
})}
|
||||
{ this.state.activeSpace === MetaSpace.Home && <AccessibleTooltipButton
|
||||
className="mx_LeftPanel_exploreButton"
|
||||
onClick={this.onExplore}
|
||||
title={space ? _t("Explore %(spaceName)s", { spaceName: space.name }) : _t("Explore rooms")}
|
||||
/>
|
||||
title={_t("Explore rooms")}
|
||||
/> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -415,10 +399,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<div className={containerClasses} ref={this.ref}>
|
||||
<aside className="mx_LeftPanel_roomListContainer">
|
||||
{ this.renderHeader() }
|
||||
{ this.renderSearchDialExplore() }
|
||||
{ this.renderBreadcrumbs() }
|
||||
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
||||
{ !this.props.isMinimized && <RoomListHeader onVisibilityChange={this.refreshStickyHeaders} /> }
|
||||
<div className="mx_LeftPanel_roomListWrapper">
|
||||
<div
|
||||
className={roomListClasses}
|
||||
|
|
|
@ -82,7 +82,6 @@ import SpaceStore from "../../stores/spaces/SpaceStore";
|
|||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import { RoomUpdateCause } from "../../stores/room-list/models";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import SecurityCustomisations from "../../customisations/Security";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import QuestionDialog from "../views/dialogs/QuestionDialog";
|
||||
|
@ -112,6 +111,10 @@ import { initSentry } from "../../sentry";
|
|||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { showSpaceInvite } from "../../utils/space";
|
||||
import GenericToast from "../views/toasts/GenericToast";
|
||||
import InfoDialog from "../views/dialogs/InfoDialog";
|
||||
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -712,16 +715,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case Action.ViewRoomDirectory: {
|
||||
if (SpaceStore.instance.activeSpace[0] === "!") {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: SpaceStore.instance.activeSpace,
|
||||
});
|
||||
} else {
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
||||
initialText: payload.initialText,
|
||||
}, 'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
}
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
||||
initialText: payload.initialText,
|
||||
}, 'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
|
||||
// View the welcome or home page if we need something to look at
|
||||
this.viewSomethingBehindModal();
|
||||
|
@ -1476,6 +1472,61 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
showNotificationsToast(false);
|
||||
}
|
||||
|
||||
if (!localStorage.getItem("mx_seen_ia_1.1_changes_toast")) {
|
||||
const key = "IA_1.1_TOAST";
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key,
|
||||
title: _t("Testing small changes"),
|
||||
props: {
|
||||
description: _t("Your feedback is wanted as we try out some design changes."),
|
||||
acceptLabel: _t("More info"),
|
||||
onAccept: () => {
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("We're testing some design changes"),
|
||||
description: <>
|
||||
<img
|
||||
src={require("../../../res/img/ia-design-changes.png")}
|
||||
width="636"
|
||||
height="303"
|
||||
alt=""
|
||||
/>
|
||||
<p>{ _t(
|
||||
"Your ongoing feedback would be very welcome, so if you see anything " +
|
||||
"different you want to comment on, <a>please let us know about it</a>. " +
|
||||
"Click your avatar to find a quick feedback link.",
|
||||
{},
|
||||
{
|
||||
a: sub => <AccessibleButton
|
||||
kind="link"
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
Modal.createTrackedDialog('Feedback Dialog', '', FeedbackDialog);
|
||||
}}
|
||||
>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
},
|
||||
) }</p>
|
||||
<p>{ _t("If you'd like to preview or test some potential upcoming changes, " +
|
||||
"there's an option in feedback to let us contact you.") }</p>
|
||||
</>,
|
||||
}, "mx_DialogDesignChanges_wrapper");
|
||||
localStorage.setItem("mx_seen_ia_1.1_changes_toast", "true");
|
||||
ToastStore.sharedInstance().dismissToast(key);
|
||||
},
|
||||
rejectLabel: _t("Dismiss"),
|
||||
onReject: () => {
|
||||
localStorage.setItem("mx_seen_ia_1.1_changes_toast", "true");
|
||||
ToastStore.sharedInstance().dismissToast(key);
|
||||
},
|
||||
},
|
||||
icon: "labs",
|
||||
component: GenericToast,
|
||||
priority: 9,
|
||||
});
|
||||
}
|
||||
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
this.setState({
|
||||
ready: true,
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
|
@ -29,7 +28,8 @@ import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCo
|
|||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/spaces";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||
import { isMac } from "../../Keyboard";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -42,7 +42,6 @@ interface IProps {
|
|||
interface IState {
|
||||
query: string;
|
||||
focused: boolean;
|
||||
inSpaces: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomSearch")
|
||||
|
@ -57,13 +56,11 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
this.state = {
|
||||
query: "",
|
||||
focused: false,
|
||||
inSpaces: false,
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||
SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
|
||||
|
@ -84,15 +81,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||
SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
|
||||
}
|
||||
|
||||
private onSpaces = (spaces: Room[]) => {
|
||||
this.setState({
|
||||
inSpaces: spaces.length > 0,
|
||||
});
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === Action.ViewRoom && payload.clear_search) {
|
||||
this.clearInput();
|
||||
|
@ -163,11 +153,6 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
|
||||
});
|
||||
|
||||
let placeholder = _t("Filter");
|
||||
if (this.state.inSpaces) {
|
||||
placeholder = _t("Filter all spaces");
|
||||
}
|
||||
|
||||
let icon = (
|
||||
<div className='mx_RoomSearch_icon' />
|
||||
);
|
||||
|
@ -181,7 +166,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
onBlur={this.onBlur}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={placeholder}
|
||||
placeholder={_t("Filter")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
|
@ -193,6 +178,9 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
onClick={this.clearInput}
|
||||
/>
|
||||
);
|
||||
let shortcutPrompt = <div className="mx_RoomSearch_shortcutPrompt">
|
||||
{ isMac ? "⌘ K" : "Ctrl K" }
|
||||
</div>;
|
||||
|
||||
if (this.props.isMinimized) {
|
||||
icon = (
|
||||
|
@ -204,12 +192,14 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
input = null;
|
||||
clearButton = null;
|
||||
shortcutPrompt = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{ icon }
|
||||
{ input }
|
||||
{ shortcutPrompt }
|
||||
{ clearButton }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -48,6 +48,7 @@ import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPan
|
|||
import { useStateArray } from "../../hooks/useStateArray";
|
||||
import SpacePublicShare from "../views/spaces/SpacePublicShare";
|
||||
import {
|
||||
shouldShowSpaceInvite,
|
||||
shouldShowSpaceSettings,
|
||||
showAddExistingRooms,
|
||||
showCreateNewRoom,
|
||||
|
@ -396,7 +397,7 @@ const SpaceLandingAddButton = ({ space }) => {
|
|||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconHash"
|
||||
iconClassName="mx_RoomList_iconAddExistingRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -440,9 +441,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
|||
const userId = cli.getUserId();
|
||||
|
||||
let inviteButton;
|
||||
if (((myMembership === "join" && space.canInvite(userId)) || space.getJoinRule() === JoinRule.Public) &&
|
||||
shouldShowComponent(UIComponent.InviteUsers)
|
||||
) {
|
||||
if (shouldShowSpaceInvite(space) && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
inviteButton = (
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import React, { createRef, useContext, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from "classnames";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
@ -25,7 +25,7 @@ import dis from "../../dispatcher/dispatcher";
|
|||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { ContextMenuButton } from "./ContextMenu";
|
||||
import { ChevronFace, ContextMenuButton } from "./ContextMenu";
|
||||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
||||
|
@ -42,27 +42,62 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
|
|||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuCheckbox,
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||
import { showCommunityInviteDialog } from "../../RoomInvite";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
|
||||
import { UIFeature } from "../../settings/UIFeature";
|
||||
import HostSignupAction from "./HostSignupAction";
|
||||
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import TooltipButton from "../views/elements/TooltipButton";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
|
||||
|
||||
const CustomStatusSection = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const setStatus = cli.getUser(cli.getUserId()).unstable_statusMessage || "";
|
||||
const [value, setValue] = useState(setStatus);
|
||||
|
||||
let details: JSX.Element;
|
||||
if (value !== setStatus) {
|
||||
details = <>
|
||||
<p>{ _t("Your status will be shown to people you have a DM with.") }</p>
|
||||
|
||||
<AccessibleButton
|
||||
onClick={() => cli._unstable_setStatusMessage(value)}
|
||||
kind="primary_outline"
|
||||
>
|
||||
{ value ? _t("Set status") : _t("Clear status") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <div className="mx_UserMenu_CustomStatusSection">
|
||||
<div className="mx_UserMenu_CustomStatusSection_input">
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
placeholder={_t("Set a new status")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<AccessibleButton
|
||||
tabIndex={-1}
|
||||
title={_t("Clear")}
|
||||
className="mx_UserMenu_CustomStatusSection_clear"
|
||||
onClick={() => setValue("")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ details }
|
||||
</div>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
isPanelCollapsed: boolean;
|
||||
}
|
||||
|
||||
type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
|
||||
|
@ -72,14 +107,30 @@ interface IState {
|
|||
isDarkTheme: boolean;
|
||||
isHighContrast: boolean;
|
||||
selectedSpace?: Room;
|
||||
pendingRoomJoin: Set<string>;
|
||||
dndEnabled: boolean;
|
||||
}
|
||||
|
||||
const toRightOf = (rect: PartialDOMRect) => {
|
||||
return {
|
||||
left: rect.width + rect.left + 8,
|
||||
top: rect.top,
|
||||
chevronFace: ChevronFace.None,
|
||||
};
|
||||
};
|
||||
|
||||
const below = (rect: PartialDOMRect) => {
|
||||
return {
|
||||
left: rect.left,
|
||||
top: rect.top + rect.height,
|
||||
chevronFace: ChevronFace.None,
|
||||
};
|
||||
};
|
||||
|
||||
@replaceableComponent("structures.UserMenu")
|
||||
export default class UserMenu extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private themeWatcherRef: string;
|
||||
private dndWatcherRef: string;
|
||||
private readonly dndWatcherRef: string;
|
||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||
private tagStoreRef: fbEmitter.EventSubscription;
|
||||
|
||||
|
@ -90,7 +141,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
contextMenuPosition: null,
|
||||
isDarkTheme: this.isUserOnDarkTheme(),
|
||||
isHighContrast: this.isUserOnHighContrastTheme(),
|
||||
pendingRoomJoin: new Set<string>(),
|
||||
dndEnabled: this.doNotDisturb,
|
||||
selectedSpace: SpaceStore.instance.activeSpaceRoom,
|
||||
};
|
||||
|
||||
|
@ -99,8 +150,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||
}
|
||||
|
||||
// Force update is the easiest way to trigger the UI update (we don't store state for this)
|
||||
this.dndWatcherRef = SettingsStore.watchSetting("doNotDisturb", null, () => this.forceUpdate());
|
||||
SettingsStore.monitorSetting("feature_dnd", null);
|
||||
SettingsStore.monitorSetting("doNotDisturb", null);
|
||||
}
|
||||
|
||||
private get doNotDisturb(): boolean {
|
||||
return SettingsStore.getValue("doNotDisturb");
|
||||
}
|
||||
|
||||
private get hasHomePage(): boolean {
|
||||
|
@ -111,7 +166,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
||||
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
|
||||
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -123,13 +177,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
if (SpaceStore.spacesEnabled) {
|
||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||
}
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
}
|
||||
|
||||
private onRoom = (room: Room): void => {
|
||||
this.removePendingJoinRoom(room.roomId);
|
||||
};
|
||||
|
||||
private onTagStoreUpdate = () => {
|
||||
this.forceUpdate(); // we don't have anything useful in state to update
|
||||
};
|
||||
|
@ -178,8 +227,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onAction = (ev: ActionPayload) => {
|
||||
switch (ev.action) {
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
case Action.ToggleUserMenu:
|
||||
if (this.state.contextMenuPosition) {
|
||||
this.setState({ contextMenuPosition: null });
|
||||
|
@ -187,36 +236,27 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
if (this.buttonRef.current) this.buttonRef.current.click();
|
||||
}
|
||||
break;
|
||||
case Action.JoinRoom:
|
||||
this.addPendingJoinRoom(ev.roomId);
|
||||
break;
|
||||
case Action.JoinRoomReady:
|
||||
case Action.JoinRoomError:
|
||||
this.removePendingJoinRoom(ev.roomId);
|
||||
break;
|
||||
|
||||
case Action.SettingUpdated: {
|
||||
const settingUpdatedPayload = payload as SettingUpdatedPayload;
|
||||
switch (settingUpdatedPayload.settingName) {
|
||||
case "feature_dnd":
|
||||
case "doNotDisturb": {
|
||||
const dndEnabled = this.doNotDisturb;
|
||||
if (this.state.dndEnabled !== dndEnabled) {
|
||||
this.setState({ dndEnabled });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private addPendingJoinRoom(roomId: string): void {
|
||||
this.setState({
|
||||
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin)
|
||||
.add(roomId),
|
||||
});
|
||||
}
|
||||
|
||||
private removePendingJoinRoom(roomId: string): void {
|
||||
if (this.state.pendingRoomJoin.delete(roomId)) {
|
||||
this.setState({
|
||||
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
|
||||
this.setState({ contextMenuPosition: ev.currentTarget.getBoundingClientRect() });
|
||||
};
|
||||
|
||||
private onContextMenu = (ev: React.MouseEvent) => {
|
||||
|
@ -262,15 +302,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
||||
private onShowArchived = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// TODO: Archived room view: https://github.com/vector-im/element-web/issues/14038
|
||||
// Note: You'll need to uncomment the button too.
|
||||
logger.log("TODO: Show archived rooms");
|
||||
};
|
||||
|
||||
private onProvideFeedback = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -312,50 +343,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
||||
private onCommunitySettingsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
|
||||
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
|
||||
});
|
||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
||||
private 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: Action.ViewRoom,
|
||||
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"),
|
||||
});
|
||||
}
|
||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
||||
private onCommunityInviteClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
||||
private onDndToggle = (ev) => {
|
||||
private onDndToggle = (ev: ButtonEvent) => {
|
||||
ev.stopPropagation();
|
||||
const current = SettingsStore.getValue("doNotDisturb");
|
||||
SettingsStore.setValue("doNotDisturb", null, SettingLevel.DEVICE, !current);
|
||||
|
@ -364,8 +352,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
private renderContextMenu = (): React.ReactNode => {
|
||||
if (!this.state.contextMenuPosition) return null;
|
||||
|
||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let topSection;
|
||||
const hostSignupConfig: IHostSignupConfig = SdkConfig.get().hostSignup;
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
|
@ -411,6 +397,24 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let customStatusSection: JSX.Element;
|
||||
if (SettingsStore.getValue("feature_custom_status")) {
|
||||
customStatusSection = <CustomStatusSection />;
|
||||
}
|
||||
|
||||
let dndButton: JSX.Element;
|
||||
if (SettingsStore.getValue("feature_dnd")) {
|
||||
dndButton = (
|
||||
<IconizedContextMenuCheckbox
|
||||
iconClassName={this.state.dndEnabled ? "mx_UserMenu_iconDnd" : "mx_UserMenu_iconDndOff"}
|
||||
label={_t("Do not disturb")}
|
||||
onClick={this.onDndToggle}
|
||||
active={this.state.dndEnabled}
|
||||
words
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let feedbackButton;
|
||||
if (SettingsStore.getValue(UIFeature.Feedback)) {
|
||||
feedbackButton = <IconizedContextMenuOption
|
||||
|
@ -420,156 +424,68 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
/>;
|
||||
}
|
||||
|
||||
let primaryHeader = (
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{ OwnProfileStore.instance.displayName }
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{ MatrixClientPeg.get().getUserId() }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
let primaryOptionList = (
|
||||
<React.Fragment>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ homeButton }
|
||||
{ dndButton }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconBell"
|
||||
label={_t("Notifications")}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconLock"
|
||||
label={_t("Security & privacy")}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("All settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{ feedbackButton }
|
||||
<IconizedContextMenuOption
|
||||
className="mx_IconizedContextMenu_option_red"
|
||||
iconClassName="mx_UserMenu_iconSignOut"
|
||||
label={_t("Sign out")}
|
||||
onClick={this.onSignOutClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
primaryOptionList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{ homeButton }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconBell"
|
||||
label={_t("Notification settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconLock"
|
||||
label={_t("Security & privacy")}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("All settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{ /* <IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconArchive"
|
||||
label={_t("Archived rooms")}
|
||||
onClick={this.onShowArchived}
|
||||
/> */ }
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSignOut"
|
||||
label={_t("Sign out")}
|
||||
onClick={this.onSignOutClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
);
|
||||
let secondarySection = null;
|
||||
|
||||
if (prototypeCommunityName) {
|
||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
primaryHeader = (
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{ prototypeCommunityName }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
let settingsOption;
|
||||
let inviteOption;
|
||||
if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
|
||||
inviteOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconInvite"
|
||||
label={_t("Invite")}
|
||||
onClick={this.onCommunityInviteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
|
||||
settingsOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
aria-label={_t("Community settings")}
|
||||
onClick={this.onCommunitySettingsClick}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
primaryOptionList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{ settingsOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMembers"
|
||||
label={_t("Members")}
|
||||
onClick={this.onCommunityMembersClick}
|
||||
/>
|
||||
{ inviteOption }
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
secondarySection = (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{ OwnProfileStore.instance.displayName }
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{ MatrixClientPeg.get().getUserId() }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
aria-label={_t("User settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSignOut"
|
||||
label={_t("Sign out")}
|
||||
onClick={this.onSignOutClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else if (MatrixClientPeg.get().isGuest()) {
|
||||
primaryOptionList = (
|
||||
<React.Fragment>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ homeButton }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
"mx_UserMenu_contextMenu": true,
|
||||
"mx_UserMenu_contextMenu_prototype": !!prototypeCommunityName,
|
||||
});
|
||||
const position = this.props.isPanelCollapsed
|
||||
? toRightOf(this.state.contextMenuPosition)
|
||||
: below(this.state.contextMenuPosition);
|
||||
|
||||
return <IconizedContextMenu
|
||||
// numerical adjustments to overlap the context menu by just over the width of the
|
||||
// menu icon and make it look connected
|
||||
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 10}
|
||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height + 8}
|
||||
{...position}
|
||||
onFinished={this.onCloseMenu}
|
||||
className={classes}
|
||||
className="mx_UserMenu_contextMenu"
|
||||
>
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
{ primaryHeader }
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{ OwnProfileStore.instance.displayName }
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{ MatrixClientPeg.get().getUserId() }
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AccessibleTooltipButton
|
||||
className="mx_UserMenu_contextMenu_themeButton"
|
||||
onClick={this.onSwitchThemeClick}
|
||||
|
@ -582,9 +498,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
{ customStatusSection }
|
||||
{ topSection }
|
||||
{ primaryOptionList }
|
||||
{ secondarySection }
|
||||
</IconizedContextMenu>;
|
||||
};
|
||||
|
||||
|
@ -595,102 +511,45 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
const displayName = OwnProfileStore.instance.displayName || userId;
|
||||
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
||||
|
||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let isPrototype = false;
|
||||
let menuName = _t("User menu");
|
||||
let name = <span className="mx_UserMenu_userName">{ displayName }</span>;
|
||||
let buttons = (
|
||||
<span className="mx_UserMenu_headerButtons">
|
||||
{ /* masked image in CSS */ }
|
||||
</span>
|
||||
);
|
||||
let dnd;
|
||||
if (this.state.selectedSpace) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
<span className="mx_UserMenu_userName">{ displayName }</span>
|
||||
<RoomName room={this.state.selectedSpace}>
|
||||
{ (roomName) => <span className="mx_UserMenu_subUserName">{ roomName }</span> }
|
||||
</RoomName>
|
||||
</div>
|
||||
);
|
||||
} else if (prototypeCommunityName) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
<span className="mx_UserMenu_userName">{ prototypeCommunityName }</span>
|
||||
<span className="mx_UserMenu_subUserName">{ displayName }</span>
|
||||
</div>
|
||||
);
|
||||
menuName = _t("Community and user menu");
|
||||
isPrototype = true;
|
||||
} else if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
<span className="mx_UserMenu_userName">{ _t("Home") }</span>
|
||||
<span className="mx_UserMenu_subUserName">{ displayName }</span>
|
||||
</div>
|
||||
);
|
||||
isPrototype = true;
|
||||
} else if (SettingsStore.getValue("feature_dnd")) {
|
||||
const isDnd = SettingsStore.getValue("doNotDisturb");
|
||||
dnd = <AccessibleButton
|
||||
onClick={this.onDndToggle}
|
||||
className={classNames({
|
||||
"mx_UserMenu_dnd": true,
|
||||
"mx_UserMenu_dnd_noisy": !isDnd,
|
||||
"mx_UserMenu_dnd_muted": isDnd,
|
||||
})}
|
||||
/>;
|
||||
}
|
||||
if (this.props.isMinimized) {
|
||||
name = null;
|
||||
buttons = null;
|
||||
let badge: JSX.Element;
|
||||
if (this.state.dndEnabled) {
|
||||
badge = <div className="mx_UserMenu_dndBadge" />;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_UserMenu': true,
|
||||
'mx_UserMenu_minimized': this.props.isMinimized,
|
||||
'mx_UserMenu_prototype': isPrototype,
|
||||
});
|
||||
let name: JSX.Element;
|
||||
if (!this.props.isPanelCollapsed) {
|
||||
name = <div className="mx_UserMenu_name">
|
||||
{ displayName }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuButton
|
||||
className={classes}
|
||||
onClick={this.onOpenMenuClick}
|
||||
inputRef={this.buttonRef}
|
||||
label={menuName}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className="mx_UserMenu_row">
|
||||
<span className="mx_UserMenu_userAvatarContainer">
|
||||
<BaseAvatar
|
||||
idName={userId}
|
||||
name={displayName}
|
||||
url={avatarUrl}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
resizeMethod="crop"
|
||||
className="mx_UserMenu_userAvatar"
|
||||
/>
|
||||
</span>
|
||||
{ name }
|
||||
{ this.state.pendingRoomJoin.size > 0 && (
|
||||
<InlineSpinner>
|
||||
<TooltipButton helpText={_t(
|
||||
"Currently joining %(count)s rooms",
|
||||
{ count: this.state.pendingRoomJoin.size },
|
||||
)} />
|
||||
</InlineSpinner>
|
||||
) }
|
||||
{ dnd }
|
||||
{ buttons }
|
||||
</div>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton
|
||||
onClick={this.onOpenMenuClick}
|
||||
inputRef={this.buttonRef}
|
||||
label={_t("User menu")}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
onContextMenu={this.onContextMenu}
|
||||
className={classNames("mx_UserMenu", {
|
||||
mx_UserMenu_cutout: badge,
|
||||
})}
|
||||
>
|
||||
<div className="mx_UserMenu_userAvatar">
|
||||
<BaseAvatar
|
||||
idName={userId}
|
||||
name={displayName}
|
||||
url={avatarUrl}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
resizeMethod="crop"
|
||||
className="mx_UserMenu_userAvatar_BaseAvatar"
|
||||
/>
|
||||
{ badge }
|
||||
</div>
|
||||
{ name }
|
||||
|
||||
{ this.renderContextMenu() }
|
||||
</React.Fragment>
|
||||
</ContextMenuButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -798,6 +798,11 @@
|
|||
"The person who invited you already left the room.": "The person who invited you already left the room.",
|
||||
"The person who invited you already left the room, or their server is offline.": "The person who invited you already left the room, or their server is offline.",
|
||||
"Failed to join room": "Failed to join room",
|
||||
"All rooms": "All rooms",
|
||||
"Home": "Home",
|
||||
"Favourites": "Favourites",
|
||||
"People": "People",
|
||||
"Other rooms": "Other rooms",
|
||||
"You joined the call": "You joined the call",
|
||||
"%(senderName)s joined the call": "%(senderName)s joined the call",
|
||||
"Call in progress": "Call in progress",
|
||||
|
@ -1048,8 +1053,6 @@
|
|||
"Quick settings": "Quick settings",
|
||||
"All settings": "All settings",
|
||||
"Pin to sidebar": "Pin to sidebar",
|
||||
"Favourites": "Favourites",
|
||||
"People": "People",
|
||||
"More options": "More options",
|
||||
"Theme": "Theme",
|
||||
"Space selection": "Space selection",
|
||||
|
@ -1086,11 +1089,8 @@
|
|||
"You can change these anytime.": "You can change these anytime.",
|
||||
"Creating...": "Creating...",
|
||||
"Create": "Create",
|
||||
"Home": "Home",
|
||||
"Show all rooms": "Show all rooms",
|
||||
"All rooms": "All rooms",
|
||||
"Options": "Options",
|
||||
"Other rooms": "Other rooms",
|
||||
"Expand space panel": "Expand space panel",
|
||||
"Collapse space panel": "Collapse space panel",
|
||||
"Collapse": "Collapse",
|
||||
|
@ -1711,16 +1711,20 @@
|
|||
"Hide Widgets": "Hide Widgets",
|
||||
"Show Widgets": "Show Widgets",
|
||||
"Search": "Search",
|
||||
"Invites": "Invites",
|
||||
"Start new chat": "Start new chat",
|
||||
"Invite to space": "Invite to space",
|
||||
"You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space",
|
||||
"Add people": "Add people",
|
||||
"Start chat": "Start chat",
|
||||
"Add room": "Add room",
|
||||
"Explore rooms": "Explore rooms",
|
||||
"Create new room": "Create new room",
|
||||
"You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space",
|
||||
"Add existing room": "Add existing room",
|
||||
"You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space",
|
||||
"Explore rooms": "Explore rooms",
|
||||
"Explore community rooms": "Explore community rooms",
|
||||
"Explore public rooms": "Explore public rooms",
|
||||
"Add room": "Add room",
|
||||
"Invites": "Invites",
|
||||
"Low priority": "Low priority",
|
||||
"System Alerts": "System Alerts",
|
||||
"Historical": "Historical",
|
||||
|
@ -1730,13 +1734,17 @@
|
|||
"Can't see what you're looking for?": "Can't see what you're looking for?",
|
||||
"Start a new chat": "Start a new chat",
|
||||
"Explore all public rooms": "Explore all public rooms",
|
||||
"Quick actions": "Quick actions",
|
||||
"Explore %(spaceName)s": "Explore %(spaceName)s",
|
||||
"Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below",
|
||||
"%(count)s results in all spaces|other": "%(count)s results in all spaces",
|
||||
"%(count)s results in all spaces|one": "%(count)s result in all spaces",
|
||||
"Community settings": "Community settings",
|
||||
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
|
||||
"Members": "Members",
|
||||
"%(count)s results|other": "%(count)s results",
|
||||
"%(count)s results|one": "%(count)s result",
|
||||
"Invite": "Invite",
|
||||
"Join public room": "Join public room",
|
||||
"Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms",
|
||||
"Currently joining %(count)s rooms|one": "Currently joining %(count)s room",
|
||||
"%(spaceName)s menu": "%(spaceName)s menu",
|
||||
"Home options": "Home options",
|
||||
"This room": "This room",
|
||||
"Joining space …": "Joining space …",
|
||||
"Joining room …": "Joining room …",
|
||||
|
@ -1882,7 +1890,6 @@
|
|||
"The homeserver the user you're verifying is connected to": "The homeserver the user you're verifying is connected to",
|
||||
"Yours, or the other users' internet connection": "Yours, or the other users' internet connection",
|
||||
"Yours, or the other users' session": "Yours, or the other users' session",
|
||||
"Members": "Members",
|
||||
"Nothing pinned, yet": "Nothing pinned, yet",
|
||||
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||
"Pinned messages": "Pinned messages",
|
||||
|
@ -1913,7 +1920,6 @@
|
|||
"Hide sessions": "Hide sessions",
|
||||
"Jump to read receipt": "Jump to read receipt",
|
||||
"Mention": "Mention",
|
||||
"Invite": "Invite",
|
||||
"Share Link to User": "Share Link to User",
|
||||
"Message": "Message",
|
||||
"Demote yourself?": "Demote yourself?",
|
||||
|
@ -2454,14 +2460,15 @@
|
|||
"Include Attachments": "Include Attachments",
|
||||
"Export": "Export",
|
||||
"Feedback sent": "Feedback sent",
|
||||
"Comment": "Comment",
|
||||
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
|
||||
"Feedback": "Feedback",
|
||||
"You may contact me if you want to follow up or to let me test out upcoming ideas": "You may contact me if you want to follow up or to let me test out upcoming ideas",
|
||||
"Rate %(brand)s": "Rate %(brand)s",
|
||||
"Tell us below how you feel about %(brand)s so far.": "Tell us below how you feel about %(brand)s so far.",
|
||||
"Please go into as much detail as you like, so we can track down the problem.": "Please go into as much detail as you like, so we can track down the problem.",
|
||||
"Add comment": "Add comment",
|
||||
"Comment": "Comment",
|
||||
"There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.",
|
||||
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
|
||||
"Feedback": "Feedback",
|
||||
"Report a bug": "Report a bug",
|
||||
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
|
||||
"Send feedback": "Send feedback",
|
||||
|
@ -2474,7 +2481,6 @@
|
|||
"Search for rooms or people": "Search for rooms or people",
|
||||
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
|
||||
"Done": "Done",
|
||||
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
|
||||
"You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
|
||||
"Confirm abort of host creation": "Confirm abort of host creation",
|
||||
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
|
||||
|
@ -2772,12 +2778,9 @@
|
|||
"Mentions only": "Mentions only",
|
||||
"Copy link": "Copy link",
|
||||
"See room timeline (devtools)": "See room timeline (devtools)",
|
||||
"Add space": "Add space",
|
||||
"Room": "Room",
|
||||
"Space": "Space",
|
||||
"Manage & explore rooms": "Manage & explore rooms",
|
||||
"Clear status": "Clear status",
|
||||
"Update status": "Update status",
|
||||
"Set status": "Set status",
|
||||
"Set a new status...": "Set a new status...",
|
||||
"Move up": "Move up",
|
||||
"Move down": "Move down",
|
||||
"View Community": "View Community",
|
||||
|
@ -2802,7 +2805,6 @@
|
|||
"Avatar": "Avatar",
|
||||
"This room is public": "This room is public",
|
||||
"Away": "Away",
|
||||
"User Status": "User Status",
|
||||
"powered by Matrix": "powered by Matrix",
|
||||
"This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
|
||||
"Country Dropdown": "Country Dropdown",
|
||||
|
@ -2929,6 +2931,12 @@
|
|||
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
|
||||
"Unable to copy room link": "Unable to copy room link",
|
||||
"Unable to copy a link to the room to the clipboard.": "Unable to copy a link to the room to the clipboard.",
|
||||
"Testing small changes": "Testing small changes",
|
||||
"Your feedback is wanted as we try out some design changes.": "Your feedback is wanted as we try out some design changes.",
|
||||
"More info": "More info",
|
||||
"We're testing some design changes": "We're testing some design changes",
|
||||
"Your ongoing feedback would be very welcome, so if you see anything different you want to comment on, <a>please let us know about it</a>. Click your avatar to find a quick feedback link.": "Your ongoing feedback would be very welcome, so if you see anything different you want to comment on, <a>please let us know about it</a>. Click your avatar to find a quick feedback link.",
|
||||
"If you'd like to preview or test some potential upcoming changes, there's an option in feedback to let us contact you.": "If you'd like to preview or test some potential upcoming changes, there's an option in feedback to let us contact you.",
|
||||
"Signed Out": "Signed Out",
|
||||
"For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.",
|
||||
"Terms and Conditions": "Terms and Conditions",
|
||||
|
@ -2974,7 +2982,6 @@
|
|||
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
|
||||
"Explore rooms in %(communityName)s": "Explore rooms in %(communityName)s",
|
||||
"Filter": "Filter",
|
||||
"Filter all spaces": "Filter all spaces",
|
||||
"Clear filter": "Clear filter",
|
||||
"Filter rooms and people": "Filter rooms and people",
|
||||
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
|
||||
|
@ -2992,7 +2999,6 @@
|
|||
"Search failed": "Search failed",
|
||||
"Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(",
|
||||
"No more results": "No more results",
|
||||
"Room": "Room",
|
||||
"Failed to reject invite": "Failed to reject invite",
|
||||
"Drop file here to upload": "Drop file here to upload",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
|
||||
|
@ -3012,7 +3018,6 @@
|
|||
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
|
||||
"Results": "Results",
|
||||
"Rooms and spaces": "Rooms and spaces",
|
||||
"Space": "Space",
|
||||
"Search names and descriptions": "Search names and descriptions",
|
||||
"Private space": "Private space",
|
||||
"<inviter/> invites you": "<inviter/> invites you",
|
||||
|
@ -3020,6 +3025,7 @@
|
|||
"To join this Space, hide communities in your <a>preferences</a>": "To join this Space, hide communities in your <a>preferences</a>",
|
||||
"To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite",
|
||||
"Created from <Community />": "Created from <Community />",
|
||||
"Add space": "Add space",
|
||||
"Welcome to <name/>": "Welcome to <name/>",
|
||||
"Random": "Random",
|
||||
"Support": "Support",
|
||||
|
@ -3066,20 +3072,19 @@
|
|||
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
|
||||
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
|
||||
"Your status will be shown to people you have a DM with.": "Your status will be shown to people you have a DM with.",
|
||||
"Set status": "Set status",
|
||||
"Clear status": "Clear status",
|
||||
"Set a new status": "Set a new status",
|
||||
"Clear": "Clear",
|
||||
"Got an account? <a>Sign in</a>": "Got an account? <a>Sign in</a>",
|
||||
"New here? <a>Create an account</a>": "New here? <a>Create an account</a>",
|
||||
"Notification settings": "Notification settings",
|
||||
"Do not disturb": "Do not disturb",
|
||||
"Security & privacy": "Security & privacy",
|
||||
"Community settings": "Community settings",
|
||||
"User settings": "User settings",
|
||||
"Switch to light mode": "Switch to light mode",
|
||||
"Switch to dark mode": "Switch to dark mode",
|
||||
"Switch theme": "Switch theme",
|
||||
"User menu": "User menu",
|
||||
"Community and user menu": "Community and user menu",
|
||||
"Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms",
|
||||
"Currently joining %(count)s rooms|one": "Currently joining %(count)s room",
|
||||
"Could not load user profile": "Could not load user profile",
|
||||
"Decrypted event source": "Decrypted event source",
|
||||
"Original event source": "Original event source",
|
||||
|
|
|
@ -288,7 +288,7 @@ export async function submitFeedback(
|
|||
body.append("user_id", MatrixClientPeg.get()?.getUserId());
|
||||
|
||||
for (const k in extraData) {
|
||||
body.append(k, extraData[k]);
|
||||
body.append(k, JSON.stringify(extraData[k]));
|
||||
}
|
||||
|
||||
await submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
|
||||
|
|
|
@ -319,6 +319,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
"doNotDisturb": {
|
||||
supportedLevels: [SettingLevel.DEVICE],
|
||||
default: false,
|
||||
controller: new IncompatibleController("feature_dnd", false, false),
|
||||
},
|
||||
"mjolnirRooms": {
|
||||
supportedLevels: [SettingLevel.ACCOUNT],
|
||||
|
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
|
||||
|
||||
import { _t } from "../../languageHandler";
|
||||
|
||||
// The consts & types are moved out here to prevent cyclical imports
|
||||
|
||||
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
||||
|
@ -33,6 +35,19 @@ export enum MetaSpace {
|
|||
Orphans = "orphans-space",
|
||||
}
|
||||
|
||||
export const getMetaSpaceName = (spaceKey: MetaSpace, allRoomsInHome = false): string => {
|
||||
switch (spaceKey) {
|
||||
case MetaSpace.Home:
|
||||
return allRoomsInHome ? _t("All rooms") : _t("Home");
|
||||
case MetaSpace.Favourites:
|
||||
return _t("Favourites");
|
||||
case MetaSpace.People:
|
||||
return _t("People");
|
||||
case MetaSpace.Orphans:
|
||||
return _t("Other rooms");
|
||||
}
|
||||
};
|
||||
|
||||
export type SpaceKey = MetaSpace | Room["roomId"];
|
||||
|
||||
export interface ISuggestedRoom extends IHierarchyRoom {
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from "react";
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import { calculateRoomVia } from "./permalinks/Permalinks";
|
||||
import Modal from "../Modal";
|
||||
|
@ -101,6 +102,10 @@ export const showCreateNewRoom = async (space: Room): Promise<boolean> => {
|
|||
return shouldCreate;
|
||||
};
|
||||
|
||||
export const shouldShowSpaceInvite = (space: Room) =>
|
||||
(space?.getMyMembership() === "join" && space.canInvite(space.client.getUserId())) ||
|
||||
space.getJoinRule() === JoinRule.Public;
|
||||
|
||||
export const showSpaceInvite = (space: Room, initialText = ""): void => {
|
||||
if (space.getJoinRule() === "public") {
|
||||
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue