diff --git a/res/css/_components.scss b/res/css/_components.scss index 15cb20d963..004984f097 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/structures/_SpaceHierarchy.scss b/res/css/structures/_SpaceHierarchy.scss index afb01d7c9a..d79eed1a64 100644 --- a/res/css/structures/_SpaceHierarchy.scss +++ b/res/css/structures/_SpaceHierarchy.scss @@ -46,6 +46,7 @@ limitations under the License. .mx_AccessibleButton_kind_link { padding: 0; + font-size: inherit; } .mx_SearchBox { diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 3119d2fe6e..37ddbcf5ef 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -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; - } - } - } -} diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index b299198349..42e17c8d98 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -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; + } +} diff --git a/res/css/views/dialogs/_BetaFeedbackDialog.scss b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss similarity index 90% rename from res/css/views/dialogs/_BetaFeedbackDialog.scss rename to res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss index 9f5f6b512e..f83eed9c53 100644 --- a/res/css/views/dialogs/_BetaFeedbackDialog.scss +++ b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss @@ -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; diff --git a/res/css/views/dialogs/_LeaveSpaceDialog.scss b/res/css/views/dialogs/_LeaveSpaceDialog.scss new file mode 100644 index 0000000000..c982f50e52 --- /dev/null +++ b/res/css/views/dialogs/_LeaveSpaceDialog.scss @@ -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; + } + } +} diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index f2ceb086c4..0c1b41ca38 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -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; diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 866bc904c2..1e25deba26 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -38,18 +38,22 @@ limitations under the License. padding-top: 0; } + &::before { + content: ''; + position: absolute; + top: -1px; + bottom: -1px; + left: -60px; + right: -60px; + z-index: -1; + border-radius: 4px; + } + &:hover, &.mx_EventTile_selected { + &::before { - content: ''; - position: absolute; - top: -1px; - bottom: -1px; - left: -60px; - right: -60px; - z-index: -1; background: $eventbubble-bg-hover; - border-radius: 4px; } .mx_EventTile_avatar { @@ -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; diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 206fe843de..5de9a9f9d1 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -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; diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index 88b9d8f693..097b2b648e 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -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; + } +} diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 407dc6f04c..01c086b73c 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -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,7 +394,13 @@ export class ContextMenu extends React.PureComponent { } render(): React.ReactChild { - return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer()); + 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()); + } } } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index f2052f1613..01feaefc6e 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -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
-
-
- { _t("Spaces are a beta feature.") } - { - if (onClick) onClick(); - Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, { - featureId: "feature_spaces", - }); - }}> - { _t("Feedback") } - -
-
; -}; - const RoomMemberCount = ({ room, children }) => { const members = useRoomMembers(room); const count = members.length; @@ -356,7 +332,7 @@ const SpaceLandingAddButton = ({ space }) => { }} /> { e.preventDefault(); @@ -432,6 +408,7 @@ const SpaceLanding = ({ space }) => { }; return
+
@@ -456,8 +433,6 @@ const SpaceLanding = ({ space }) => {
) } - -
; @@ -542,7 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { value={buttonLabel} /> - ; }; @@ -567,7 +541,6 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => { spacesRenderer={defaultSpacesRenderer} dmsRenderer={defaultDmsRenderer} /> - ; }; @@ -587,7 +560,6 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRoom { createdRooms ? _t("Go to my first room") : _t("Go to my space") } - ; }; @@ -616,9 +588,8 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {

{ _t("Teammates might not be able to view or join any private rooms you make.") }

-

{ _t("We're working on this as part of the beta, but just want to let you know.") }

+

{ _t("We're working on this, but just want to let you know.") }

- ; }; @@ -741,7 +712,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => { value={buttonLabel} /> - ; }; diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index b1c09f2b22..97f45167a8 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -103,8 +103,8 @@ export default class CaptchaForm extends React.Component = ({ space, onCreateSubspaceCl footerPrompt={<>
{ _t("Want to add a new space instead?") }
- { _t("Create a new subspace") } + { _t("Create a new space") } } filterPlaceholder={_t("Search for spaces")} spacesRenderer={defaultSpacesRenderer} /> - - onFinished(false)} /> ; }; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 9b64af0c80..cf4f369d09 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -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 = ({ space, onCreateRoomClick, dmsRenderer={defaultDmsRenderer} /> - - onFinished(false)} /> ; }; diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 34218a3399..c5fba52b51 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -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,77 +34,28 @@ interface IProps extends IDialogProps { const BetaFeedbackDialog: React.FC = ({ 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 ( -
- { _t(info.feedbackSubheading) } -   - { _t("Your platform and username will be noted to help us use your feedback as much as we can.") } - - { - onFinished(false); - defaultDispatcher.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Labs, - }); - }} - > - { _t("To leave the beta, visit your settings.") } - -
- - { - setComment(ev.target.value); - }} - autoFocus={true} - /> - - setCanContact((e.target as HTMLInputElement).checked)} - > - { _t("You may contact me if you have any follow up questions") } - - } - button={_t("Send feedback")} - buttonDisabled={!comment} - onFinished={sendFeedback} - />); + subheading={_t(info.feedbackSubheading)} + onFinished={onFinished} + rageshakeLabel={info.feedbackLabel} + rageshakeData={Object.fromEntries((SettingsStore.getBetaInfo(featureId)?.extraSettings || []).map(k => { + return SettingsStore.getValue(k); + }))} + > + { + onFinished(false); + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Labs, + }); + }} + > + { _t("To leave the beta, visit your settings.") } + + ; }; export default BetaFeedbackDialog; diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 6a81bb04df..0d71eb2de3 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -136,7 +136,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick return = ({ space, onAddExistingSpaceClick
- { _t("Add a subspace to a space you manage.") } + { _t("Add a space to a space you manage.") }
= ({ space, onAddExistingSpaceClick aliasFieldRef={spaceAliasField} > ; +} + +const GenericFeatureFeedbackDialog: React.FC = ({ + 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 ( +
+ { subheading } +   + { _t("Your platform and username will be noted to help us use your feedback as much as we can.") } + + { children } +
+ + { + setComment(ev.target.value); + }} + autoFocus={true} + /> + + setCanContact((e.target as HTMLInputElement).checked)} + > + { _t("You may contact me if you have any follow up questions") } + + } + button={_t("Send feedback")} + buttonDisabled={!comment} + onFinished={sendFeedback} + />); +}; + +export default GenericFeatureFeedbackDialog; diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx new file mode 100644 index 0000000000..6e1e798e9d --- /dev/null +++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx @@ -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(rooms, { + keys: ["name"], + funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)], + shouldMatchWordsOnly: false, + }); + + return matcher.match(lcQuery); + }, [rooms, lcQuery]); + + return
+ + + { filteredRooms.map(room => { + return { + onChange(checked, room); + }} + />; + }) } + { filteredRooms.length < 1 ? + { _t("No results") } + : undefined } + +
; +}; + +const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave }) => { + const selected = useMemo(() => new Set(roomsToLeave), [roomsToLeave]); + const [state, setState] = useState(RoomsToLeave.All); + + useEffect(() => { + if (state === RoomsToLeave.All) { + setRoomsToLeave(spaceChildren); + } else { + setRoomsToLeave([]); + } + }, [setRoomsToLeave, state, spaceChildren]); + + return
+ + + { state === RoomsToLeave.Specific && ( + { + if (selected) { + setRoomsToLeave([room, ...roomsToLeave]); + } else { + setRoomsToLeave(roomsToLeave.filter(r => r !== room)); + } + }} + /> + ) } +
; +}; + +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 = ({ space, onFinished }) => { + const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]); + const [roomsToLeave, setRoomsToLeave] = useState([]); + + 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 onFinished(false)} + fixedWidth={false} + > +
+

+ { _t("Are you sure you want to leave ?", {}, { + spaceName: () => { space.name }, + }) } +   + { rejoinWarning } +

+ + { spaceChildren.length > 0 && } + + { onlyAdminWarning &&
+ { onlyAdminWarning } +
} +
+ onFinished(true, roomsToLeave)} + hasCancel={true} + onCancel={onFinished} + /> +
; +}; + +export default LeaveSpaceDialog; diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index aba1d443a9..95d29fc9ae 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -192,7 +192,8 @@ class Pill extends React.Component { }); } - onUserPillClicked = () => { + onUserPillClicked = (e) => { + e.preventDefault(); dis.dispatch({ action: Action.ViewUser, member: this.state.member, diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 538b524f0f..f682f1c8c0 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -366,7 +366,7 @@ export default class MImageBody extends React.Component { } const thumbnail = ( -
+
{ showPlaceholder &&
{ 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
+ { _t("Spaces are a new feature.") } + { + 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.") } + +
; +}; + type BProps = Pick, "setAvatar" | "name" | "setName" | "topic" | "setTopic">; interface ISpaceCreateFormProps extends BProps { busy: boolean; @@ -280,13 +307,6 @@ const SpaceCreateMenu = ({ onFinished }) => { managed={false} > - { - onFinished(); - defaultDispatcher.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Labs, - }); - }} /> { body } ; diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx index a43b180752..595bdb2448 100644 --- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx +++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx @@ -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 &&
{ error }
} - -
{ - defaultDispatcher.dispatch({ - action: "leave_room", - room_id: space.roomId, - }); + leaveSpace(space); }} > { _t("Leave Space") } diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 2312494641..827fc6bde1 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -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 { 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 { /> diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 481d7b303c..356e642d65 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -115,6 +115,7 @@ export default class CallView extends React.Component { private controlsHideTimer: number = null; private dialpadButton = createRef(); private contextMenuButton = createRef(); + private contextMenu = createRef(); constructor(props: IProps) { super(props); @@ -545,12 +546,42 @@ export default class CallView extends React.Component { ); } + let dialPad; + if (this.state.showDialpad) { + dialPad = ; + } + + let contextMenu; + if (this.state.showMoreMenu) { + contextMenu = ; + } + return (
+ { dialPad } + { contextMenu } { dialpadButton } { myClassName = 'mx_CallView_pip'; } - let dialPad; - if (this.state.showDialpad) { - dialPad = ; - } - - let contextMenu; - if (this.state.showMoreMenu) { - contextMenu = ; - } - return
{ header } { contentView } - { dialPad } - { contextMenu }
; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a960af9f79..64a3fe2951 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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 will be able to find and join.": "Anyone in will be able to find and join.", "Anyone will be able to find and join this space, not just members of .": "Anyone will be able to find and join this space, not just members of .", "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 debug logs to help us track down the problem.": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", + "Feedback": "Feedback", "Report a bug": "Report a bug", "Please view existing bugs on Github first. No match? Start a new one.": "Please view existing bugs on Github first. No match? Start a new one.", + "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 ?": "Are you sure you want to leave ?", "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", " invites you": " invites you", "To view %(spaceName)s, turn on the Spaces beta": "To view %(spaceName)s, turn on the Spaces beta", @@ -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", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 5aa49df8a1..64edd4c202 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -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 diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index bf8c1ecb44..24b3c46474 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -331,7 +331,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }, 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 => { diff --git a/src/utils/objects.ts b/src/utils/objects.ts index c2ee6ce100..e3b7b6cf59 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -141,21 +141,3 @@ export function objectKeyChanges(a: O, b: O): (keyof O)[] { export function objectClone(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(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; -} diff --git a/src/utils/space.tsx b/src/utils/space.tsx index e705b4eee4..fecb581e65 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -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"); +}; diff --git a/test/utils/arrays-test.ts b/test/utils/arrays-test.ts index cf9a5f0089..277260bf29 100644 --- a/test/utils/arrays-test.ts +++ b/test/utils/arrays-test.ts @@ -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); }); }); diff --git a/test/utils/objects-test.ts b/test/utils/objects-test.ts index 154fa3604f..b360fbd1d1 100644 --- a/test/utils/objects-test.ts +++ b/test/utils/objects-test.ts @@ -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); - }); - }); });