Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18089

 Conflicts:
	src/i18n/strings/en_EN.json
This commit is contained in:
Michael Telatynski 2021-07-30 11:34:13 +01:00
commit 9eb0986353
33 changed files with 660 additions and 357 deletions

View file

@ -67,7 +67,6 @@
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
@import "./views/dialogs/_AddressPickerDialog.scss";
@import "./views/dialogs/_Analytics.scss";
@import "./views/dialogs/_BetaFeedbackDialog.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@ -82,12 +81,14 @@
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
@import "./views/dialogs/_FeedbackDialog.scss";
@import "./views/dialogs/_ForwardDialog.scss";
@import "./views/dialogs/_GenericFeatureFeedbackDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_HostSignupDialog.scss";
@import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss";
@import "./views/dialogs/_JoinRuleDropdown.scss";
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
@import "./views/dialogs/_LeaveSpaceDialog.scss";
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_ModalWidgetDialog.scss";

View file

@ -46,6 +46,7 @@ limitations under the License.
.mx_AccessibleButton_kind_link {
padding: 0;
font-size: inherit;
}
.mx_SearchBox {

View file

@ -335,24 +335,17 @@ $SpaceRoomViewInnerWidth: 428px;
word-wrap: break-word;
}
> hr {
border: none;
height: 1px;
background-color: $groupFilterPanel-bg-color;
}
.mx_SearchBox {
margin: 0 0 20px;
flex: 0;
}
.mx_SpaceFeedbackPrompt {
margin-bottom: 16px;
// hide the HR as we have our own
& + hr {
display: none;
}
padding: 7px; // 8px - 1px border
border: 1px solid $menu-border-color;
border-radius: 8px;
width: max-content;
margin: 0 0 -40px auto; // collapse its own height to not push other components down
}
}
@ -508,66 +501,3 @@ $SpaceRoomViewInnerWidth: 428px;
}
}
}
.mx_SpaceFeedbackPrompt {
margin-top: 18px;
margin-bottom: 12px;
> hr {
border: none;
border-top: 1px solid $input-border-color;
margin-bottom: 12px;
}
> div {
display: flex;
flex-direction: row;
font-size: $font-15px;
line-height: $font-24px;
> span {
color: $secondary-fg-color;
position: relative;
padding-left: 32px;
font-size: inherit;
line-height: inherit;
margin-right: auto;
&::before {
content: '';
position: absolute;
left: 0;
top: 2px;
height: 20px;
width: 20px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
}
}
.mx_AccessibleButton_kind_link {
color: $accent-color;
position: relative;
padding: 0 0 0 24px;
margin-left: 8px;
font-size: inherit;
line-height: inherit;
&::before {
content: '';
position: absolute;
left: 0;
height: 16px;
width: 16px;
background-color: $accent-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/chat-bubbles.svg');
mask-position: center;
}
}
}
}

View file

@ -50,35 +50,6 @@ limitations under the License.
line-height: $font-15px;
}
.mx_AddExistingToSpace_entry {
display: flex;
margin-top: 12px;
.mx_DecoratedRoomAvatar, // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
.mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom {
margin-right: 12px;
}
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 8px;
}
.mx_AddExistingToSpace_entry_name {
font-size: $font-15px;
line-height: 30px;
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 12px;
}
.mx_Checkbox {
align-items: center;
}
}
.mx_AccessibleButton_kind_link {
font-size: $font-12px;
line-height: $font-15px;
@ -255,3 +226,32 @@ limitations under the License.
line-height: $font-24px;
}
}
.mx_AddExistingToSpace_entry {
display: flex;
margin-top: 12px;
.mx_DecoratedRoomAvatar, // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
.mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom {
margin-right: 12px;
}
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 8px;
}
.mx_AddExistingToSpace_entry_name {
font-size: $font-15px;
line-height: 30px;
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 12px;
}
.mx_Checkbox {
align-items: center;
}
}

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_BetaFeedbackDialog {
.mx_BetaFeedbackDialog_subheading {
.mx_GenericFeatureFeedbackDialog {
.mx_GenericFeatureFeedbackDialog_subheading {
color: $primary-fg-color;
font-size: $font-14px;
line-height: $font-20px;

View file

@ -0,0 +1,96 @@
/*
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.
*/
.mx_LeaveSpaceDialog_wrapper {
.mx_Dialog {
display: flex;
flex-direction: column;
padding: 24px 32px;
}
}
.mx_LeaveSpaceDialog {
width: 440px;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
max-height: 520px;
.mx_Dialog_content {
flex-grow: 1;
margin: 0;
overflow-y: auto;
.mx_RadioButton + .mx_RadioButton {
margin-top: 16px;
}
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
border-radius: 8px;
}
.mx_LeaveSpaceDialog_noResults {
display: block;
margin-top: 24px;
}
.mx_LeaveSpaceDialog_section {
margin: 16px 0;
}
.mx_LeaveSpaceDialog_section_warning {
position: relative;
border-radius: 8px;
margin: 12px 0 0;
padding: 12px 8px 12px 42px;
background-color: $header-panel-bg-color;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
&::before {
content: '';
position: absolute;
left: 10px;
top: calc(50% - 8px); // vertical centering
height: 16px;
width: 16px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
}
}
> p {
color: $primary-fg-color;
}
}
.mx_Dialog_buttons {
margin-top: 20px;
.mx_Dialog_primary {
background-color: $notice-primary-color !important; // override default colour
border-color: $notice-primary-color;
}
}
}

View file

@ -23,7 +23,7 @@ limitations under the License.
background-color: $dark-panel-bg-color;
border-radius: 8px;
margin: 10px auto;
max-width: 75%;
width: 75%;
box-sizing: border-box;
height: 60px;

View file

@ -38,8 +38,6 @@ limitations under the License.
padding-top: 0;
}
&:hover,
&.mx_EventTile_selected {
&::before {
content: '';
position: absolute;
@ -48,10 +46,16 @@ limitations under the License.
left: -60px;
right: -60px;
z-index: -1;
background: $eventbubble-bg-hover;
border-radius: 4px;
}
&:hover,
&.mx_EventTile_selected {
&::before {
background: $eventbubble-bg-hover;
}
.mx_EventTile_avatar {
img {
box-shadow: 0 0 0 3px $eventbubble-bg-hover;
@ -276,6 +280,11 @@ limitations under the License.
margin-right: 5px;
}
.mx_EventTile_line,
.mx_EventTile_info {
min-width: 100%;
}
.mx_EventTile_e2eIcon {
margin-left: 9px;
}
@ -288,9 +297,9 @@ limitations under the License.
}
.mx_EventListSummary[data-layout=bubble] {
--maxWidth: 80%;
--maxWidth: 70%;
margin-left: calc(var(--avatarSize) + var(--gutterSize));
margin-right: calc(var(--gutterSize) + var(--avatarSize));
margin-right: 94px;
.mx_EventListSummary_toggle {
float: none;
margin: 0;

View file

@ -59,7 +59,6 @@ $hover-select-border: 4px;
font-size: $font-14px;
display: inline-block; /* anti-zalgo, with overflow hidden */
overflow: hidden;
cursor: pointer;
padding-bottom: 0px;
padding-top: 0px;
margin: 0px;
@ -322,6 +321,10 @@ $hover-select-border: 4px;
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
}
.mx_SenderProfile {
cursor: pointer;
}
.mx_EventTile_bubbleContainer {
display: grid;
grid-template-columns: 1fr 100px;

View file

@ -43,6 +43,12 @@ $spacePanelWidth: 71px;
color: $secondary-fg-color;
margin: 0;
}
.mx_SpaceFeedbackPrompt {
border-top: 1px solid $input-border-color;
padding-top: 12px;
margin-top: 16px;
}
}
// XXX remove this when spaces leaves Beta
@ -99,3 +105,25 @@ $spacePanelWidth: 71px;
}
}
}
.mx_SpaceFeedbackPrompt {
font-size: $font-15px;
line-height: $font-24px;
> span {
color: $secondary-fg-color;
position: relative;
font-size: inherit;
line-height: inherit;
margin-right: auto;
}
.mx_AccessibleButton_kind_link {
color: $accent-color;
position: relative;
padding: 0;
margin-left: 8px;
font-size: inherit;
line-height: inherit;
}
}

View file

@ -80,6 +80,10 @@ export interface IProps extends IPosition {
managed?: boolean;
wrapperClassName?: string;
// If true, this context menu will be mounted as a child to the parent container. Otherwise
// it will be mounted to a container at the root of the DOM.
mountAsChild?: boolean;
// Function to be called on menu close
onFinished();
// on resize callback
@ -390,9 +394,15 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
}
render(): React.ReactChild {
if (this.props.mountAsChild) {
// Render as a child of the current parent
return this.renderMenu();
} else {
// Render as a child of a container at the root of the DOM
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
}
}
}
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">, chevronOffset = 12) => {

View file

@ -72,10 +72,8 @@ import IconizedContextMenu, {
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { BetaPill } from "../views/beta/BetaCard";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import Modal from "../../Modal";
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
import SdkConfig from "../../SdkConfig";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
interface IProps {
space: Room;
@ -102,28 +100,6 @@ enum Phase {
PrivateExistingRooms,
}
// XXX: Temporary for the Spaces Beta only
export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
if (!SdkConfig.get().bug_report_endpoint_url) return null;
return <div className="mx_SpaceFeedbackPrompt">
<hr />
<div>
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
<AccessibleButton
kind="link"
onClick={() => {
if (onClick) onClick();
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
featureId: "feature_spaces",
});
}}>
{ _t("Feedback") }
</AccessibleButton>
</div>
</div>;
};
const RoomMemberCount = ({ room, children }) => {
const members = useRoomMembers(room);
const count = members.length;
@ -356,7 +332,7 @@ const SpaceLandingAddButton = ({ space }) => {
}}
/>
<IconizedContextMenuOption
label={_t("Add subspace")}
label={_t("Add space")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
@ -432,6 +408,7 @@ const SpaceLanding = ({ space }) => {
};
return <div className="mx_SpaceRoomView_landing">
<SpaceFeedbackPrompt />
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<div className="mx_SpaceRoomView_landing_name">
<RoomName room={space}>
@ -456,8 +433,6 @@ const SpaceLanding = ({ space }) => {
</div>
) }
</RoomTopic>
<SpaceFeedbackPrompt />
<hr />
<SpaceHierarchy space={space} showRoom={showRoom} additionalButtons={addRoomButton} />
</div>;
@ -542,7 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
value={buttonLabel}
/>
</div>
<SpaceFeedbackPrompt />
</div>;
};
@ -567,7 +541,6 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
spacesRenderer={defaultSpacesRenderer}
dmsRenderer={defaultDmsRenderer}
/>
<SpaceFeedbackPrompt />
</div>;
};
@ -587,7 +560,6 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRoom
{ createdRooms ? _t("Go to my first room") : _t("Go to my space") }
</AccessibleButton>
</div>
<SpaceFeedbackPrompt />
</div>;
};
@ -616,9 +588,8 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
</AccessibleButton>
<div className="mx_SpaceRoomView_betaWarning">
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
<p>{ _t("We're working on this, but just want to let you know.") }</p>
</div>
<SpaceFeedbackPrompt />
</div>;
};
@ -741,7 +712,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
value={buttonLabel}
/>
</div>
<SpaceFeedbackPrompt />
</div>;
};

View file

@ -103,8 +103,8 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
}
private resetRecaptcha() {
if (this.captchaWidgetId !== null) {
global.grecaptcha.reset(this.captchaWidgetId);
if (this.captchaWidgetId) {
global?.grecaptcha?.reset(this.captchaWidgetId);
}
}

View file

@ -27,6 +27,8 @@ import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig";
import SettingsFlag from "../elements/SettingsFlag";
// XXX: Keep this around for re-use in future Betas
interface IProps {
title?: string;
featureId: string;

View file

@ -21,7 +21,6 @@ import { _t } from '../../../languageHandler';
import BaseDialog from "./BaseDialog";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import { AddExistingToSpace, defaultSpacesRenderer, SubspaceSelector } from "./AddExistingToSpaceDialog";
interface IProps {
@ -54,15 +53,13 @@ const AddExistingSubspaceDialog: React.FC<IProps> = ({ space, onCreateSubspaceCl
footerPrompt={<>
<div>{ _t("Want to add a new space instead?") }</div>
<AccessibleButton onClick={onCreateSubspaceClick} kind="link">
{ _t("Create a new subspace") }
{ _t("Create a new space") }
</AccessibleButton>
</>}
filterPlaceholder={_t("Search for spaces")}
spacesRenderer={defaultSpacesRenderer}
/>
</MatrixClientContext.Provider>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
</BaseDialog>;
};

View file

@ -35,7 +35,6 @@ import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
@ -446,8 +445,6 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ space, onCreateRoomClick,
dmsRenderer={defaultDmsRenderer}
/>
</MatrixClientContext.Provider>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
</BaseDialog>;
};

View file

@ -14,22 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState } from "react";
import React from "react";
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
import SdkConfig from "../../../SdkConfig";
import { IDialogProps } from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import { submitFeedback } from "../../../rageshake/submit-rageshake";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserSettingsDialog";
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
// XXX: Keep this around for re-use in future Betas
interface IProps extends IDialogProps {
featureId: string;
@ -38,39 +34,15 @@ interface IProps extends IDialogProps {
const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
const info = SettingsStore.getBetaInfo(featureId);
const [comment, setComment] = useState("");
const [canContact, setCanContact] = useState(false);
const sendFeedback = async (ok: boolean) => {
if (!ok) return onFinished(false);
const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => {
o[k] = SettingsStore.getValue(k);
return o;
}, {});
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData);
onFinished(true);
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
title: _t("Beta feedback"),
description: _t("Thank you for your feedback, we really appreciate it."),
button: _t("Done"),
hasCloseButton: false,
fixedWidth: false,
});
};
return (<QuestionDialog
className="mx_BetaFeedbackDialog"
hasCancelButton={true}
return <GenericFeatureFeedbackDialog
title={_t("%(featureName)s beta feedback", { featureName: info.title })}
description={<React.Fragment>
<div className="mx_BetaFeedbackDialog_subheading">
{ _t(info.feedbackSubheading) }
&nbsp;
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
subheading={_t(info.feedbackSubheading)}
onFinished={onFinished}
rageshakeLabel={info.feedbackLabel}
rageshakeData={Object.fromEntries((SettingsStore.getBetaInfo(featureId)?.extraSettings || []).map(k => {
return SettingsStore.getValue(k);
}))}
>
<AccessibleButton
kind="link"
onClick={() => {
@ -83,32 +55,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
>
{ _t("To leave the beta, visit your settings.") }
</AccessibleButton>
</div>
<Field
id="feedbackComment"
label={_t("Feedback")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
autoFocus={true}
/>
<StyledCheckbox
checked={canContact}
onClick={e => setCanContact((e.target as HTMLInputElement).checked)}
>
{ _t("You may contact me if you have any follow up questions") }
</StyledCheckbox>
</React.Fragment>}
button={_t("Send feedback")}
buttonDisabled={!comment}
onFinished={sendFeedback}
/>);
</GenericFeatureFeedbackDialog>;
};
export default BetaFeedbackDialog;

View file

@ -136,7 +136,7 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
return <BaseDialog
title={(
<SubspaceSelector
title={_t("Create a subspace")}
title={_t("Create a space")}
space={space}
value={parentSpace}
onChange={setParentSpace}
@ -151,7 +151,7 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
<div className="mx_CreateSubspaceDialog_content">
<div className="mx_CreateSubspaceDialog_betaNotice">
<BetaPill />
{ _t("Add a subspace to a space you manage.") }
{ _t("Add a space to a space you manage.") }
</div>
<SpaceCreateForm
@ -169,9 +169,9 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
aliasFieldRef={spaceAliasField}
>
<JoinRuleDropdown
label={_t("Subspace visibility")}
labelInvite={_t("Private subspace (invite only)")}
labelPublic={_t("Public subspace")}
label={_t("Space visibility")}
labelInvite={_t("Private space (invite only)")}
labelPublic={_t("Public space")}
labelRestricted={supportsRestricted ? _t("Visible to space members") : undefined}
width={478}
value={joinRule}

View file

@ -0,0 +1,101 @@
/*
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, { useState } from "react";
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
import SdkConfig from "../../../SdkConfig";
import { IDialogProps } from "./IDialogProps";
import { submitFeedback } from "../../../rageshake/submit-rageshake";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
interface IProps extends IDialogProps {
title: string;
subheading: string;
rageshakeLabel: string;
rageshakeData?: Record<string, string>;
}
const GenericFeatureFeedbackDialog: React.FC<IProps> = ({
title,
subheading,
children,
rageshakeLabel,
rageshakeData = {},
onFinished,
}) => {
const [comment, setComment] = useState("");
const [canContact, setCanContact] = useState(false);
const sendFeedback = async (ok: boolean) => {
if (!ok) return onFinished(false);
submitFeedback(SdkConfig.get().bug_report_endpoint_url, rageshakeLabel, comment, canContact, rageshakeData);
onFinished(true);
Modal.createTrackedDialog("Feedback Sent", rageshakeLabel, InfoDialog, {
title,
description: _t("Thank you for your feedback, we really appreciate it."),
button: _t("Done"),
hasCloseButton: false,
fixedWidth: false,
});
};
return (<QuestionDialog
className="mx_GenericFeatureFeedbackDialog"
hasCancelButton={true}
title={title}
description={<React.Fragment>
<div className="mx_GenericFeatureFeedbackDialog_subheading">
{ subheading }
&nbsp;
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
{ children }
</div>
<Field
id="feedbackComment"
label={_t("Feedback")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
autoFocus={true}
/>
<StyledCheckbox
checked={canContact}
onChange={e => setCanContact((e.target as HTMLInputElement).checked)}
>
{ _t("You may contact me if you have any follow up questions") }
</StyledCheckbox>
</React.Fragment>}
button={_t("Send feedback")}
buttonDisabled={!comment}
onFinished={sendFeedback}
/>);
};
export default GenericFeatureFeedbackDialog;

View file

@ -0,0 +1,197 @@
/*
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, { useEffect, useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t } from '../../../languageHandler';
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { Entry } from "./AddExistingToSpaceDialog";
import SearchBox from "../../structures/SearchBox";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import StyledRadioGroup from "../elements/StyledRadioGroup";
enum RoomsToLeave {
All = "All",
Specific = "Specific",
None = "None",
}
const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase().trim();
const filteredRooms = useMemo(() => {
if (!lcQuery) {
return rooms;
}
const matcher = new QueryMatcher<Room>(rooms, {
keys: ["name"],
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
shouldMatchWordsOnly: false,
});
return matcher.match(lcQuery);
}, [rooms, lcQuery]);
return <div className="mx_LeaveSpaceDialog_section">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={filterPlaceholder}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
{ filteredRooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={(checked) => {
onChange(checked, room);
}}
/>;
}) }
{ filteredRooms.length < 1 ? <span className="mx_LeaveSpaceDialog_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
</div>;
};
const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave }) => {
const selected = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
const [state, setState] = useState<string>(RoomsToLeave.All);
useEffect(() => {
if (state === RoomsToLeave.All) {
setRoomsToLeave(spaceChildren);
} else {
setRoomsToLeave([]);
}
}, [setRoomsToLeave, state, spaceChildren]);
return <div className="mx_LeaveSpaceDialog_section">
<StyledRadioGroup
name="roomsToLeave"
value={state}
onChange={setState}
definitions={[
{
value: RoomsToLeave.All,
label: _t("Leave all rooms and spaces"),
}, {
value: RoomsToLeave.None,
label: _t("Don't leave any"),
}, {
value: RoomsToLeave.Specific,
label: _t("Leave specific rooms and spaces"),
},
]}
/>
{ state === RoomsToLeave.Specific && (
<SpaceChildPicker
filterPlaceholder={_t("Search %(spaceName)s", { spaceName: space.name })}
rooms={spaceChildren}
selected={selected}
onChange={(selected: boolean, room: Room) => {
if (selected) {
setRoomsToLeave([room, ...roomsToLeave]);
} else {
setRoomsToLeave(roomsToLeave.filter(r => r !== room));
}
}}
/>
) }
</div>;
};
interface IProps {
space: Room;
onFinished(leave: boolean, rooms?: Room[]): void;
}
const isOnlyAdmin = (room: Room): boolean => {
return !room.getJoinedMembers().some(member => {
return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100;
});
};
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]);
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
let rejoinWarning;
if (space.getJoinRule() !== JoinRule.Public) {
rejoinWarning = _t("You won't be able to rejoin unless you are re-invited.");
}
let onlyAdminWarning;
if (isOnlyAdmin(space)) {
onlyAdminWarning = _t("You're the only admin of this space. " +
"Leaving it will mean no one has control over it.");
} else {
const numChildrenOnlyAdminIn = roomsToLeave.filter(isOnlyAdmin).length;
if (numChildrenOnlyAdminIn > 0) {
onlyAdminWarning = _t("You're the only admin of some of the rooms or spaces you wish to leave. " +
"Leaving them will leave them without any admins.");
}
}
return <BaseDialog
title={_t("Leave %(spaceName)s", { spaceName: space.name })}
className="mx_LeaveSpaceDialog"
contentId="mx_LeaveSpaceDialog"
onFinished={() => onFinished(false)}
fixedWidth={false}
>
<div className="mx_Dialog_content" id="mx_LeaveSpaceDialog">
<p>
{ _t("Are you sure you want to leave <spaceName/>?", {}, {
spaceName: () => <b>{ space.name }</b>,
}) }
&nbsp;
{ rejoinWarning }
</p>
{ spaceChildren.length > 0 && <LeaveRoomsPicker
space={space}
spaceChildren={spaceChildren}
roomsToLeave={roomsToLeave}
setRoomsToLeave={setRoomsToLeave}
/> }
{ onlyAdminWarning && <div className="mx_LeaveSpaceDialog_section_warning">
{ onlyAdminWarning }
</div> }
</div>
<DialogButtons
primaryButton={_t("Leave space")}
onPrimaryButtonClick={() => onFinished(true, roomsToLeave)}
hasCancel={true}
onCancel={onFinished}
/>
</BaseDialog>;
};
export default LeaveSpaceDialog;

View file

@ -192,7 +192,8 @@ class Pill extends React.Component {
});
}
onUserPillClicked = () => {
onUserPillClicked = (e) => {
e.preventDefault();
dis.dispatch({
action: Action.ViewUser,
member: this.state.member,

View file

@ -366,7 +366,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
const thumbnail = (
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px", maxWidth: maxWidth + "px" }}>
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight, maxWidth: maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
{ showPlaceholder &&
<div
className="mx_MImageBody_thumbnail"

View file

@ -26,15 +26,14 @@ import createRoom from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings";
import AccessibleButton from "../elements/AccessibleButton";
import { BetaPill } from "../beta/BetaCard";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field";
import withValidation from "../elements/Validation";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
import RoomAliasField from "../elements/RoomAliasField";
import SdkConfig from "../../../SdkConfig";
import Modal from "../../../Modal";
import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog";
import SettingsStore from "../../../settings/SettingsStore";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return (
@ -65,6 +64,34 @@ const nameToAlias = (name: string, domain: string): string => {
return `#${localpart}:${domain}`;
};
// XXX: Temporary for the Spaces release only
export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
if (!SdkConfig.get().bug_report_endpoint_url) return null;
return <div className="mx_SpaceFeedbackPrompt">
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a new feature.") }</span>
<AccessibleButton
kind="link"
onClick={() => {
if (onClick) onClick();
Modal.createTrackedDialog("Spaces Feedback", "", GenericFeatureFeedbackDialog, {
title: _t("Spaces feedback"),
subheading: _t("Thank you for trying Spaces. " +
"Your feedback will help inform the next versions."),
rageshakeLabel: "spaces-feedback",
rageshakeData: Object.fromEntries([
"feature_spaces.all_rooms",
"feature_spaces.space_member_dms",
"feature_spaces.space_dm_badges",
].map(k => [k, SettingsStore.getValue(k)])),
});
}}
>
{ _t("Give feedback.") }
</AccessibleButton>
</div>;
};
type BProps = Pick<ComponentProps<typeof SpaceBasicSettings>, "setAvatar" | "name" | "setName" | "topic" | "setTopic">;
interface ISpaceCreateFormProps extends BProps {
busy: boolean;
@ -280,13 +307,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
managed={false}
>
<FocusLock returnFocus={true}>
<BetaPill onClick={() => {
onFinished();
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
}} />
{ body }
</FocusLock>
</ContextMenu>;

View file

@ -21,12 +21,11 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import SpaceBasicSettings from "./SpaceBasicSettings";
import { avatarUrlForRoom } from "../../../Avatar";
import { IDialogProps } from "../dialogs/IDialogProps";
import { getTopic } from "../elements/RoomTopic";
import { defaultDispatcher } from "../../../dispatcher/dispatcher";
import { leaveSpace } from "../../../utils/space";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@ -96,8 +95,6 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<SpaceFeedbackPrompt />
<div className="mx_SettingsTab_section">
<SpaceBasicSettings
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
@ -128,10 +125,7 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp
<AccessibleButton
kind="danger"
onClick={() => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: space.roomId,
});
leaveSpace(space);
}}
>
{ _t("Leave Space") }

View file

@ -31,6 +31,7 @@ import { _t } from "../../../languageHandler";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { toRightOf } from "../../structures/ContextMenu";
import {
leaveSpace,
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
@ -213,10 +214,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch({
action: "leave_room",
room_id: this.props.space.roomId,
});
leaveSpace(this.props.space);
this.setState({ contextMenuPosition: null }); // also close the menu
};
@ -330,7 +328,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Add subspace")}
label={_t("Add space")}
onClick={this.onNewSubspaceClick}
>
<BetaPill />

View file

@ -115,6 +115,7 @@ export default class CallView extends React.Component<IProps, IState> {
private controlsHideTimer: number = null;
private dialpadButton = createRef<HTMLDivElement>();
private contextMenuButton = createRef<HTMLDivElement>();
private contextMenu = createRef<HTMLDivElement>();
constructor(props: IProps) {
super(props);
@ -545,12 +546,42 @@ export default class CallView extends React.Component<IProps, IState> {
);
}
let dialPad;
if (this.state.showDialpad) {
dialPad = <DialpadContextMenu
{...alwaysAboveRightOf(
this.dialpadButton.current.getBoundingClientRect(),
ChevronFace.None,
CONTEXT_MENU_VPADDING,
)}
mountAsChild={true}
onFinished={this.closeDialpad}
call={this.props.call}
/>;
}
let contextMenu;
if (this.state.showMoreMenu) {
contextMenu = <CallContextMenu
{...alwaysAboveLeftOf(
this.contextMenuButton.current.getBoundingClientRect(),
ChevronFace.None,
CONTEXT_MENU_VPADDING,
)}
mountAsChild={true}
onFinished={this.closeContextMenu}
call={this.props.call}
/>;
}
return (
<div
className={callControlsClasses}
onMouseEnter={this.onCallControlsMouseEnter}
onMouseLeave={this.onCallControlsMouseLeave}
>
{ dialPad }
{ contextMenu }
{ dialpadButton }
<AccessibleButton
className={micClasses}
@ -858,37 +889,9 @@ export default class CallView extends React.Component<IProps, IState> {
myClassName = 'mx_CallView_pip';
}
let dialPad;
if (this.state.showDialpad) {
dialPad = <DialpadContextMenu
{...alwaysAboveRightOf(
this.dialpadButton.current.getBoundingClientRect(),
ChevronFace.None,
CONTEXT_MENU_VPADDING,
)}
onFinished={this.closeDialpad}
call={this.props.call}
/>;
}
let contextMenu;
if (this.state.showMoreMenu) {
contextMenu = <CallContextMenu
{...alwaysAboveLeftOf(
this.contextMenuButton.current.getBoundingClientRect(),
ChevronFace.None,
CONTEXT_MENU_VPADDING,
)}
onFinished={this.closeContextMenu}
call={this.props.call}
/>;
}
return <div className={"mx_CallView " + myClassName}>
{ header }
{ contentView }
{ dialPad }
{ contextMenu }
</div>;
}
}

View file

@ -1005,6 +1005,10 @@
"Name": "Name",
"Description": "Description",
"Please enter a name for the space": "Please enter a name for the space",
"Spaces are a new feature.": "Spaces are a new feature.",
"Spaces feedback": "Spaces feedback",
"Thank you for trying Spaces. Your feedback will help inform the next versions.": "Thank you for trying Spaces. Your feedback will help inform the next versions.",
"Give feedback.": "Give feedback.",
"e.g. my-space": "e.g. my-space",
"Address": "Address",
"Create a space": "Create a space",
@ -1056,7 +1060,7 @@
"Leave space": "Leave space",
"Create new room": "Create new room",
"Add existing room": "Add existing room",
"Add subspace": "Add subspace",
"Add space": "Add space",
"Members": "Members",
"Manage & explore rooms": "Manage & explore rooms",
"Explore rooms": "Explore rooms",
@ -2113,7 +2117,7 @@
"Matrix rooms": "Matrix rooms",
"Add existing space": "Add existing space",
"Want to add a new space instead?": "Want to add a new space instead?",
"Create a new subspace": "Create a new subspace",
"Create a new space": "Create a new space",
"Search for spaces": "Search for spaces",
"Not all selected were added": "Not all selected were added",
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
@ -2138,15 +2142,8 @@
"Invite anyway and never warn me again": "Invite anyway and never warn me again",
"Invite anyway": "Invite anyway",
"Close dialog": "Close dialog",
"Beta feedback": "Beta feedback",
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
"Done": "Done",
"%(featureName)s beta feedback": "%(featureName)s beta feedback",
"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.",
"To leave the beta, visit your settings.": "To leave the beta, visit your settings.",
"Feedback": "Feedback",
"You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
"Send feedback": "Send feedback",
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.",
"Preparing to send logs": "Preparing to send logs",
"Logs sent": "Logs sent",
@ -2222,11 +2219,10 @@
"Anyone in <SpaceName/> will be able to find and join.": "Anyone in <SpaceName/> will be able to find and join.",
"Anyone will be able to find and join this space, not just members of <SpaceName/>.": "Anyone will be able to find and join this space, not just members of <SpaceName/>.",
"Only people invited will be able to find and join this space.": "Only people invited will be able to find and join this space.",
"Create a subspace": "Create a subspace",
"Add a subspace to a space you manage.": "Add a subspace to a space you manage.",
"Subspace visibility": "Subspace visibility",
"Private subspace (invite only)": "Private subspace (invite only)",
"Public subspace": "Public subspace",
"Add a space to a space you manage.": "Add a space to a space you manage.",
"Space visibility": "Space visibility",
"Private space (invite only)": "Private space (invite only)",
"Public space": "Public space",
"Want to add an existing space instead?": "Want to add an existing space instead?",
"Adding...": "Adding...",
"Sign out": "Sign out",
@ -2294,8 +2290,10 @@
"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",
"You don't have permission to do this": "You don't have permission to do this",
"Sending": "Sending",
"Sent": "Sent",
@ -2303,6 +2301,10 @@
"Forward message": "Forward message",
"Message preview": "Message preview",
"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.",
"Abort": "Abort",
@ -2374,6 +2376,15 @@
"Clear cache and resync": "Clear cache and resync",
"%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
"Updating %(brand)s": "Updating %(brand)s",
"Leave all rooms and spaces": "Leave all rooms and spaces",
"Don't leave any": "Don't leave any",
"Leave specific rooms and spaces": "Leave specific rooms and spaces",
"Search %(spaceName)s": "Search %(spaceName)s",
"You won't be able to rejoin unless you are re-invited.": "You won't be able to rejoin unless you are re-invited.",
"You're the only admin of this space. Leaving it will mean no one has control over it.": "You're the only admin of this space. Leaving it will mean no one has control over it.",
"You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.": "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.",
"Leave %(spaceName)s": "Leave %(spaceName)s",
"Are you sure you want to leave <spaceName/>?": "Are you sure you want to leave <spaceName/>?",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
"Start using Key Backup": "Start using Key Backup",
"I don't want my encrypted messages": "I don't want my encrypted messages",
@ -2812,8 +2823,6 @@
"No results found": "No results found",
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
"Search names and descriptions": "Search names and descriptions",
"Spaces are a beta feature.": "Spaces are a beta feature.",
"Public space": "Public space",
"Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you",
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>",
@ -2840,7 +2849,7 @@
"Me and my teammates": "Me and my teammates",
"A private space for you and your teammates": "A private space for you and your teammates",
"Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.",
"We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.",
"We're working on this, but just want to let you know.": "We're working on this, but just want to let you know.",
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
"Inviting...": "Inviting...",
"Invite your teammates": "Invite your teammates",

View file

@ -124,6 +124,7 @@ export interface ISetting {
// not use this for new settings.
invertedSettingName?: string;
// XXX: Keep this around for re-use in future Betas
betaInfo?: {
title: string; // _td
caption: string; // _td

View file

@ -331,7 +331,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}, roomId);
}
private getChildren(spaceId: string): Room[] {
public getChildren(spaceId: string): Room[] {
const room = this.matrixClient?.getRoom(spaceId);
const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via);
return sortBy(childEvents, ev => {

View file

@ -141,21 +141,3 @@ export function objectKeyChanges<O extends {}>(a: O, b: O): (keyof O)[] {
export function objectClone<O extends {}>(obj: O): O {
return JSON.parse(JSON.stringify(obj));
}
/**
* Converts a series of entries to an object.
* @param entries The entries to convert.
* @returns The converted object.
*/
// NOTE: Deprecated once we have Object.fromEntries() support.
// @ts-ignore - return type is complaining about non-string keys, but we know better
export function objectFromEntries<K, V>(entries: Iterable<[K, V]>): {[k: K]: V} {
const obj: {
// @ts-ignore - same as return type
[k: K]: V;} = {};
for (const e of entries) {
// @ts-ignore - same as return type
obj[e[0]] = e[1];
}
return obj;
}

View file

@ -33,6 +33,10 @@ import AddExistingSubspaceDialog from "../components/views/dialogs/AddExistingSu
import defaultDispatcher from "../dispatcher/dispatcher";
import RoomViewStore from "../stores/RoomViewStore";
import { Action } from "../dispatcher/actions";
import { leaveRoomBehaviour } from "./membership";
import Spinner from "../components/views/elements/Spinner";
import dis from "../dispatcher/dispatcher";
import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog";
export const shouldShowSpaceSettings = (space: Room) => {
const userId = space.client.getUserId();
@ -148,3 +152,24 @@ export const showCreateNewSubspace = (space: Room): void => {
"mx_CreateSubspaceDialog_wrapper",
);
};
export const leaveSpace = (space: Room) => {
Modal.createTrackedDialog("Leave Space", "", LeaveSpaceDialog, {
space,
onFinished: async (leave: boolean, rooms: Room[]) => {
if (!leave) return;
const modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner");
try {
await Promise.all(rooms.map(r => leaveRoomBehaviour(r.roomId)));
await leaveRoomBehaviour(space.roomId);
} finally {
modal.close();
}
dis.dispatch({
action: "after_leave_room",
room_id: space.roomId,
});
},
}, "mx_LeaveSpaceDialog_wrapper");
};

View file

@ -29,7 +29,6 @@ import {
ArrayUtil,
GroupedArray,
} from "../../src/utils/arrays";
import { objectFromEntries } from "../../src/utils/objects";
function expectSample(i: number, input: number[], expected: number[], smooth = false) {
console.log(`Resample case index: ${i}`); // for debugging test failures
@ -336,7 +335,7 @@ describe('arrays', () => {
expect(result).toBeDefined();
expect(result.value).toBeDefined();
const asObject = objectFromEntries(result.value.entries());
const asObject = Object.fromEntries(result.value.entries());
expect(asObject).toMatchObject(output);
});
});

View file

@ -18,7 +18,6 @@ import {
objectClone,
objectDiff,
objectExcluding,
objectFromEntries,
objectHasDiff,
objectKeyChanges,
objectShallowClone,
@ -242,21 +241,4 @@ describe('objects', () => {
expect(result.test.third).not.toBe(a.test.third);
});
});
describe('objectFromEntries', () => {
it('should create an object from an array of entries', () => {
const output = { a: 1, b: 2, c: 3 };
const result = objectFromEntries(Object.entries(output));
expect(result).toBeDefined();
expect(result).toMatchObject(output);
});
it('should maintain pointers in values', () => {
const output = { a: {}, b: 2, c: 3 };
const result = objectFromEntries(Object.entries(output));
expect(result).toBeDefined();
expect(result).toMatchObject(output);
expect(result['a']).toBe(output.a);
});
});
});