From fd202d064bea9baec46bf020e87a9006619aa449 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 27 Nov 2020 14:31:16 +0000 Subject: [PATCH 01/94] Create Labs flag for Matrix Spaces (feature_spaces) --- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.ts | 11 +++++ .../controllers/IncompatibleController.ts | 46 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 src/settings/controllers/IncompatibleController.ts diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0d50128f32f..31e939778bc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -755,6 +755,7 @@ "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "Change notification settings": "Change notification settings", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags", "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.", "New spinner design": "New spinner design", "Message Pinning": "Message Pinning", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index cc6fd29fe3d..14ee8e5009a 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -36,6 +36,7 @@ import { isMac } from '../Keyboard'; import UIFeatureController from "./controllers/UIFeatureController"; import { UIFeature } from "./UIFeature"; import { OrderedMultiController } from "./controllers/OrderedMultiController"; +import IncompatibleController from "./controllers/IncompatibleController"; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -117,6 +118,13 @@ export interface ISetting { } export const SETTINGS: {[setting: string]: ISetting} = { + "feature_spaces": { + isFeature: true, + displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags"), + supportedLevels: LEVELS_FEATURE, + default: false, + controller: new ReloadOnChangeController(), + }, "feature_communities_v2_prototypes": { isFeature: true, displayName: _td( @@ -125,6 +133,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { ), supportedLevels: LEVELS_FEATURE, default: false, + controller: new IncompatibleController("feature_spaces"), }, "feature_new_spinner": { isFeature: true, @@ -150,6 +159,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"), supportedLevels: LEVELS_FEATURE, default: false, + controller: new IncompatibleController("feature_spaces"), }, "feature_state_counters": { isFeature: true, @@ -699,6 +709,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { [UIFeature.Communities]: { supportedLevels: LEVELS_UI_FEATURE, default: true, + controller: new IncompatibleController("feature_spaces"), }, [UIFeature.AdvancedSettings]: { supportedLevels: LEVELS_UI_FEATURE, diff --git a/src/settings/controllers/IncompatibleController.ts b/src/settings/controllers/IncompatibleController.ts new file mode 100644 index 00000000000..0a016ff0e03 --- /dev/null +++ b/src/settings/controllers/IncompatibleController.ts @@ -0,0 +1,46 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import SettingController from "./SettingController"; +import { SettingLevel } from "../SettingLevel"; +import SettingsStore from "../SettingsStore"; + +/** + * Enforces that a boolean setting cannot be enabled if the incompatible setting + * is also enabled, to prevent cascading undefined behaviour between conflicting + * labs flags. + */ +export default class IncompatibleController extends SettingController { + public constructor(private settingName: string, private forcedValue = false) { + super(); + } + + public getValueOverride( + level: SettingLevel, + roomId: string, + calculatedValue: any, + calculatedAtLevel: SettingLevel, + ): any { + if (this.incompatibleSettingEnabled) { + return this.forcedValue; + } + return null; // no override + } + + public get incompatibleSettingEnabled(): boolean { + return SettingsStore.getValue(this.settingName); + } +} From 715cbbe793a5cc10a923e6693c297b3c9b81591c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Dec 2020 14:50:35 +0000 Subject: [PATCH 02/94] Break out generic Room name and topic components which self maintain --- src/components/views/elements/RoomName.tsx | 37 ++++++++++++++ src/components/views/elements/RoomTopic.tsx | 42 ++++++++++++++++ src/components/views/rooms/RoomHeader.js | 56 ++++++--------------- 3 files changed, 93 insertions(+), 42 deletions(-) create mode 100644 src/components/views/elements/RoomName.tsx create mode 100644 src/components/views/elements/RoomTopic.tsx diff --git a/src/components/views/elements/RoomName.tsx b/src/components/views/elements/RoomName.tsx new file mode 100644 index 00000000000..c6b01041f6d --- /dev/null +++ b/src/components/views/elements/RoomName.tsx @@ -0,0 +1,37 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {useState} from "react"; +import {Room} from "matrix-js-sdk/src/models/room"; + +import {useEventEmitter} from "../../../hooks/useEventEmitter"; + +interface IProps { + room: Room; + children?(name: string): JSX.Element; +} + +const RoomName = ({ room, children }: IProps): JSX.Element => { + const [name, setName] = useState(room?.name); + useEventEmitter(room, "Room.name", () => { + setName(room?.name); + }); + + if (children) return children(name); + return name || ""; +}; + +export default RoomName; diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx new file mode 100644 index 00000000000..da753653835 --- /dev/null +++ b/src/components/views/elements/RoomTopic.tsx @@ -0,0 +1,42 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {useState} from "react"; +import {EventType} from "matrix-js-sdk/src/@types/event"; +import {Room} from "matrix-js-sdk/src/models/room"; + +import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import {linkifyElement} from "../../../HtmlUtils"; + +interface IProps { + room?: Room; + children?(topic: string, ref: (element: HTMLElement) => void): JSX.Element; +} + +const getTopic = room => room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic; + +const RoomTopic = ({ room, children }: IProps): JSX.Element => { + const [topic, setTopic] = useState(getTopic(room)); + useEventEmitter(room.currentState, "RoomState.events", () => { + setTopic(getTopic(room)); + }); + + const ref = e => e && linkifyElement(e); + if (children) return children(topic, ref); + return { topic }; +}; + +export default RoomTopic; diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 8eb82766302..8ca0dae615f 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -15,14 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import RateLimitedFunc from '../../../ratelimitedfunc'; -import { linkifyElement } from '../../../HtmlUtils'; import {CancelButton} from './SimpleRoomHeader'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; @@ -30,6 +29,8 @@ import E2EIcon from './E2EIcon'; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import {DefaultTagID} from "../../../stores/room-list/models"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +import RoomTopic from "../elements/RoomTopic"; +import RoomName from "../elements/RoomName"; export default class RoomHeader extends React.Component { static propTypes = { @@ -52,35 +53,13 @@ export default class RoomHeader extends React.Component { onCancelClick: null, }; - constructor(props) { - super(props); - - this._topic = createRef(); - } - componentDidMount() { const cli = MatrixClientPeg.get(); cli.on("RoomState.events", this._onRoomStateEvents); cli.on("Room.accountData", this._onRoomAccountData); - - // When a room name occurs, RoomState.events is fired *before* - // room.name is updated. So we have to listen to Room.name as well as - // RoomState.events. - if (this.props.room) { - this.props.room.on("Room.name", this._onRoomNameChange); - } - } - - componentDidUpdate() { - if (this._topic.current) { - linkifyElement(this._topic.current); - } } componentWillUnmount() { - if (this.props.room) { - this.props.room.removeListener("Room.name", this._onRoomNameChange); - } const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.events", this._onRoomStateEvents); @@ -109,10 +88,6 @@ export default class RoomHeader extends React.Component { this.forceUpdate(); }, 500); - _onRoomNameChange = (room) => { - this.forceUpdate(); - }; - _hasUnreadPins() { const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); if (!currentPinEvent) return false; @@ -170,29 +145,26 @@ export default class RoomHeader extends React.Component { } } - let roomName = _t("Join Room"); + let oobName = _t("Join Room"); if (this.props.oobData && this.props.oobData.name) { - roomName = this.props.oobData.name; - } else if (this.props.room) { - roomName = this.props.room.name; + oobName = this.props.oobData.name; } const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint }); const name =
-
{ roomName }
+ + {(name) => { + const roomName = name || oobName; + return
{ roomName }
; + }} +
{ searchStatus }
; - let topic; - if (this.props.room) { - const ev = this.props.room.currentState.getStateEvents('m.room.topic', ''); - if (ev) { - topic = ev.getContent().topic; - } - } - const topicElement = -
{ topic }
; + const topicElement = + {(topic, ref) =>
{ topic }
} +
; let roomAvatar; if (this.props.room) { From 8ee88a87ad12f56d05b1e4973de9c596f0cc9c4c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Dec 2020 15:22:55 +0000 Subject: [PATCH 03/94] Fix RoomName and RoomTopic HOCs edge case when room prop changes --- src/components/views/elements/RoomName.tsx | 5 ++++- src/components/views/elements/RoomTopic.tsx | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/RoomName.tsx b/src/components/views/elements/RoomName.tsx index c6b01041f6d..4ed7360fb7a 100644 --- a/src/components/views/elements/RoomName.tsx +++ b/src/components/views/elements/RoomName.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {useState} from "react"; +import {useEffect, useState} from "react"; import {Room} from "matrix-js-sdk/src/models/room"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; @@ -29,6 +29,9 @@ const RoomName = ({ room, children }: IProps): JSX.Element => { useEventEmitter(room, "Room.name", () => { setName(room?.name); }); + useEffect(() => { + setName(room?.name); + }, [room]); if (children) return children(name); return name || ""; diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx index da753653835..b4b59e250cb 100644 --- a/src/components/views/elements/RoomTopic.tsx +++ b/src/components/views/elements/RoomTopic.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useState} from "react"; +import React, {useEffect, useState} from "react"; import {EventType} from "matrix-js-sdk/src/@types/event"; import {Room} from "matrix-js-sdk/src/models/room"; @@ -26,13 +26,16 @@ interface IProps { children?(topic: string, ref: (element: HTMLElement) => void): JSX.Element; } -const getTopic = room => room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic; +export const getTopic = room => room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic; const RoomTopic = ({ room, children }: IProps): JSX.Element => { const [topic, setTopic] = useState(getTopic(room)); useEventEmitter(room.currentState, "RoomState.events", () => { setTopic(getTopic(room)); }); + useEffect(() => { + setTopic(getTopic(room)); + }, [room]); const ref = e => e && linkifyElement(e); if (children) return children(topic, ref); From 355c19cc25ca9ce3b23e2a58131b3f0fda9a4052 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Dec 2020 15:29:38 +0000 Subject: [PATCH 04/94] Fix room-list algorithm behaviour on space-rooms and improve edge-case error --- src/stores/room-list/algorithms/Algorithm.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 25059aabe76..23008314dc4 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -186,6 +186,9 @@ export class Algorithm extends EventEmitter { } private async doUpdateStickyRoom(val: Room) { + // no-op sticky rooms + if (val?.isSpaceRoom()) val = null; + // Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing, // otherwise we risk duplicating rooms. @@ -211,7 +214,7 @@ export class Algorithm extends EventEmitter { } // When we do have a room though, we expect to be able to find it - let tag = this.roomIdsToTags[val.roomId][0]; + let tag = this.roomIdsToTags[val.roomId]?.[0]; if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`); // We specifically do NOT use the ordered rooms set as it contains the sticky room, which From db3559542254dfab1b9f8c7685fddadd14e9f082 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Dec 2020 15:30:11 +0000 Subject: [PATCH 05/94] Ignore space-rooms as breadcrumbs --- src/stores/BreadcrumbsStore.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 24906f678cb..07e66f45662 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -122,6 +122,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } private async appendRoom(room: Room) { + if (room.isSpaceRoom()) return; // hide space rooms from breadcrumbs let updated = false; const rooms = (this.state.rooms || []).slice(); // cheap clone From 3c16d4abb8d5fb67bff4d5ae3d11900f1a28fa79 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Dec 2020 15:30:36 +0000 Subject: [PATCH 06/94] Use new Room::canInvite method to prevent duplicating code --- src/components/views/rooms/NewRoomIntro.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 9be3d6be180..ce426a64ed0 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -100,15 +100,8 @@ const NewRoomIntro = () => { }); } - let canInvite = inRoom; - const powerLevels = room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent(); - const me = room.getMember(cli.getUserId()); - if (powerLevels && me && powerLevels.invite > me.powerLevel) { - canInvite = false; - } - let buttons; - if (canInvite) { + if (room.canInvite(cli.getUserId())) { const onInviteClick = () => { dis.dispatch({ action: "view_invite", roomId }); }; From 4484e16e9231f12f9b353af46fd876aa3b1fe4f7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Dec 2020 15:34:20 +0000 Subject: [PATCH 07/94] Wire up RoomView prop `justCreated` for being able to distinguish post-creation CTAs --- src/components/structures/LoggedInView.tsx | 2 ++ src/components/structures/MatrixChat.tsx | 4 ++++ src/components/structures/RoomView.tsx | 1 + src/createRoom.ts | 1 + 4 files changed, 8 insertions(+) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index ec5afd13f0d..8e9b8b3f775 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -90,6 +90,7 @@ interface IProps { currentGroupId?: string; currentGroupIsNew?: boolean; justRegistered?: boolean; + roomJustCreated?: boolean; } interface IUsageLimit { @@ -574,6 +575,7 @@ class LoggedInView extends React.Component { viaServers={this.props.viaServers} key={this.props.currentRoomId || 'roomview'} resizeNotifier={this.props.resizeNotifier} + justCreated={this.props.roomJustCreated} />; break; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 4a8d3cc7189..67f6912043d 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -142,6 +142,8 @@ interface IRoomInfo { oob_data?: object; via_servers?: string[]; threepid_invite?: IThreepidInvite; + + justCreated?: boolean; } /* eslint-enable camelcase */ @@ -199,6 +201,7 @@ interface IState { viaServers?: string[]; pendingInitialSync?: boolean; justRegistered?: boolean; + roomJustCreated?: boolean; } export default class MatrixChat extends React.PureComponent { @@ -870,6 +873,7 @@ export default class MatrixChat extends React.PureComponent { roomOobData: roomInfo.oob_data, viaServers: roomInfo.via_servers, ready: true, + roomJustCreated: roomInfo.justCreated || false, }, () => { this.notifyNewScreen('room/' + presentedId, replaceLast); }); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 0ee847fbc99..e4fb2a29074 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -112,6 +112,7 @@ interface IProps { autoJoin?: boolean; resizeNotifier: ResizeNotifier; + justCreated?: boolean; // Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU) onRegistered?(credentials: IMatrixClientCreds): void; diff --git a/src/createRoom.ts b/src/createRoom.ts index 699df0d799a..508cfcf5e6f 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -205,6 +205,7 @@ export default function createRoom(opts: IOpts): Promise { // so we are expecting the room to come down the sync // stream, if it hasn't already. joining: true, + justCreated: true, }); } CountlyAnalytics.instance.trackRoomCreate(startTime, roomId); From 842cf8749397c5f70dfbf94a8cb27d2faa2aebfe Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 29 Dec 2020 14:33:33 +0000 Subject: [PATCH 08/94] Extend ContextMenu with wrapperClassName prop --- src/components/structures/ContextMenu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 190b231b747..70c54ec2b4b 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -76,6 +76,7 @@ export interface IProps extends IPosition { hasBackground?: boolean; // whether this context menu should be focus managed. If false it must handle itself managed?: boolean; + wrapperClassName?: string; // Function to be called on menu close onFinished(); @@ -365,7 +366,7 @@ export class ContextMenu extends React.PureComponent { return (
Date: Tue, 29 Dec 2020 14:35:52 +0000 Subject: [PATCH 09/94] Fix RoomAvatar typescript props definition --- src/components/views/avatars/RoomAvatar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 98d69a63e75..952b9d4cb68 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,7 +13,7 @@ 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 from 'react'; +import React, {ComponentProps} from 'react'; import Room from 'matrix-js-sdk/src/models/room'; import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo'; @@ -24,7 +24,7 @@ import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; import {ResizeMethod} from "../../../Avatar"; -interface IProps { +interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) From ee69d717994fb7269f48f2a66a172fe17bf7bdfe Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 29 Dec 2020 14:38:33 +0000 Subject: [PATCH 10/94] Avatar don't check Spaces for DM-ness --- src/Avatar.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avatar.ts b/src/Avatar.ts index 60bdfdcf753..e2557e21a88 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -165,6 +165,9 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi return explicitRoomAvatar; } + // space rooms cannot be DMs so skip the rest + if (room.isSpaceRoom()) return null; + let otherMember = null; const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); if (otherUserId) { From 110745f46680ed0e88e225d3cc886d2e54f8d853 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 29 Dec 2020 14:38:53 +0000 Subject: [PATCH 11/94] Permalinks expose serverCandidates for re-use --- src/utils/permalinks/Permalinks.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.js index e157ecc55e7..eb657ef38b8 100644 --- a/src/utils/permalinks/Permalinks.js +++ b/src/utils/permalinks/Permalinks.js @@ -121,6 +121,10 @@ export class RoomPermalinkCreator { this._started = false; } + get serverCandidates() { + return this._serverCandidates; + } + isStarted() { return this._started; } From 7454858bd8f0ea07d5407cdf9331acd51c296422 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Dec 2020 15:08:02 +0000 Subject: [PATCH 12/94] Room List algorithm - sanely handle space invites --- src/stores/room-list/algorithms/Algorithm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 23008314dc4..40fdae5ae42 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -187,7 +187,7 @@ export class Algorithm extends EventEmitter { private async doUpdateStickyRoom(val: Room) { // no-op sticky rooms - if (val?.isSpaceRoom()) val = null; + if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") val = null; // Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing, // otherwise we risk duplicating rooms. From 7f41735859a20d63cbcf89bcb6d8b2e14ec48238 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 5 Jan 2021 13:56:13 +0000 Subject: [PATCH 13/94] Add useStateArray hook --- src/hooks/useStateArray.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/hooks/useStateArray.ts diff --git a/src/hooks/useStateArray.ts b/src/hooks/useStateArray.ts new file mode 100644 index 00000000000..9f2110485a0 --- /dev/null +++ b/src/hooks/useStateArray.ts @@ -0,0 +1,27 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {useState} from "react"; + +// Hook to simplify managing state of arrays of a common type +export const useStateArray = (initialState: T, initialSize: number): [T[], (i: number, v: T) => void] => { + const [data, setData] = useState(new Array(initialSize).fill(initialState)); + return [data, (index: number, value: T) => setData(data => { + const copy = [...data]; + copy[index] = value; + return copy; + })] +}; From 570385402b714a0edc2c22741be51d65a8fac840 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 5 Jan 2021 13:58:24 +0000 Subject: [PATCH 14/94] Extend permalink utility for calculating room vias --- src/utils/permalinks/Permalinks.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.js index eb657ef38b8..4c993de03dd 100644 --- a/src/utils/permalinks/Permalinks.js +++ b/src/utils/permalinks/Permalinks.js @@ -14,9 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from "../../MatrixClientPeg"; import isIp from "is-ip"; -import * as utils from 'matrix-js-sdk/src/utils'; +import * as utils from "matrix-js-sdk/src/utils"; +import {Room} from "matrix-js-sdk/src/models/room"; + +import {MatrixClientPeg} from "../../MatrixClientPeg"; import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor"; import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor"; import ElementPermalinkConstructor from "./ElementPermalinkConstructor"; @@ -438,3 +440,9 @@ function isHostnameIpAddress(hostname) { return isIp(hostname); } + +export const calculateRoomVia = (room: Room) => { + const permalinkCreator = new RoomPermalinkCreator(room); + permalinkCreator.load(); + return permalinkCreator.serverCandidates; +}; From 55689e271fc114ebf96e954bebb045a1512e2a0a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 5 Jan 2021 13:59:03 +0000 Subject: [PATCH 15/94] Add setHasDiff utility --- src/utils/sets.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/utils/sets.ts diff --git a/src/utils/sets.ts b/src/utils/sets.ts new file mode 100644 index 00000000000..90694e5b90f --- /dev/null +++ b/src/utils/sets.ts @@ -0,0 +1,35 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + +/** + * Determines if two sets are different through a shallow comparison. + * @param a The first set. Must be defined. + * @param b The second set. Must be defined. + * @returns True if they are different, false otherwise. + */ +export function setHasDiff(a: Set, b: Set): boolean { + if (a.size === b.size) { + // When the lengths are equal, check to see if either set is missing an element from the other. + if (Array.from(b).some(i => !a.has(i))) return true; + if (Array.from(a).some(i => !b.has(i))) return true; + + // if all the keys are common, say so + return false; + } else { + return true; // different lengths means they are naturally diverged + } +} From 41ac8da488ec94bea9feb9a77c4a771890d84e4c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 5 Jan 2021 15:22:22 +0000 Subject: [PATCH 16/94] RoomListStore pass filtered lists to ListNotificationStates --- src/stores/room-list/RoomListStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index b2fe630760c..db7e3647e20 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -58,8 +58,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient { private filterConditions: IFilterCondition[] = []; private tagWatcher = new TagWatcher(this); private updateFn = new MarkedExecution(() => { - for (const tagId of Object.keys(this.unfilteredLists)) { - RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.unfilteredLists[tagId]); + for (const tagId of Object.keys(this.orderedLists)) { + RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.orderedLists[tagId]); } this.emit(LISTS_UPDATE_EVENT); }); From 13a283e2f82abe397fa068e048315aa756152bba Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 6 Jan 2021 15:51:48 +0000 Subject: [PATCH 17/94] Fix invite.svg alignment issue --- res/img/element-icons/room/invite.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/img/element-icons/room/invite.svg b/res/img/element-icons/room/invite.svg index 655f9f118a7..d2ecb837b23 100644 --- a/res/img/element-icons/room/invite.svg +++ b/res/img/element-icons/room/invite.svg @@ -1,3 +1,3 @@ - + From c16a7cfc92163deeae975e090319cd19bf71c63d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 6 Jan 2021 15:57:35 +0000 Subject: [PATCH 18/94] Iterate UserMenu styling, padding, and add an Avatar to it --- res/css/structures/_UserMenu.scss | 31 +++++++++++++++-- src/components/structures/UserMenu.tsx | 47 +++++++++++++++++++------- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 84c21364ce8..f6fa998fd40 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -72,6 +72,7 @@ limitations under the License. position: relative; // to make default avatars work margin-right: 8px; height: 32px; // to remove the unknown 4px gap the browser puts below it + padding: 3px 0; // to align with and without using doubleName .mx_UserMenu_userAvatar { border-radius: 32px; // should match avatar size @@ -180,7 +181,7 @@ limitations under the License. } .mx_UserMenu_contextMenu_header { - padding: 20px; + padding: 16px; // Create a flexbox to organize the header a bit easier display: flex; @@ -190,7 +191,7 @@ limitations under the License. // Create another flexbox of columns to handle large user IDs display: flex; flex-direction: column; - width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button + width: calc(100% - 80px); // 80px = 32px user avatar + 32px theme button + 2* 8px margin each * { // Automatically grow all subelements to fit the container @@ -215,6 +216,10 @@ limitations under the License. } } + .mx_UserMenu_contextMenu_userAvatar { + margin-right: 8px; + } + .mx_UserMenu_contextMenu_themeButton { min-width: 32px; max-width: 32px; @@ -254,6 +259,28 @@ limitations under the License. padding: 0; } } + + &.mx_UserMenu_contextMenu_secondaryHeader { + position: relative; + + &::before { + border-top: 1px solid $primary-fg-color; + opacity: 0.1; + content: ''; + + // Counteract the padding problems (width: 100% ignores the 40px padding, + // unless we position it absolutely then it does the right thing). + width: 100%; + position: absolute; + left: 0; + top: 0; + } + } + + } + + .mx_IconizedContextMenu_optionList:not(.mx_IconizedContextMenu_optionList_notFirst):nth-child(n + 2)::before { + content: unset !important; // hide the default hairlines from IconizedContextMenu } .mx_IconizedContextMenu_icon { diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 08bd472225a..a06b7944220 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -15,13 +15,18 @@ limitations under the License. */ import React, { createRef } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import classNames from "classnames"; +import * as fbEmitter from "fbemitter"; + import { MatrixClientPeg } from "../../MatrixClientPeg"; import defaultDispatcher from "../../dispatcher/dispatcher"; +import dis from "../../dispatcher/dispatcher"; import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { _t } from "../../languageHandler"; import { ContextMenuButton } from "./ContextMenu"; -import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog"; +import { USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB } from "../views/dialogs/UserSettingsDialog"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import Modal from "../../Modal"; @@ -31,11 +36,10 @@ import {getCustomTheme} from "../../theme"; import {getHostingLink} from "../../utils/HostingLink"; import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; import SdkConfig from "../../SdkConfig"; -import {getHomePageUrl} from "../../utils/pages"; +import { getHomePageUrl } from "../../utils/pages"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import BaseAvatar from '../views/avatars/BaseAvatar'; -import classNames from "classnames"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { SettingLevel } from "../../settings/SettingLevel"; import IconizedContextMenu, { @@ -43,14 +47,20 @@ import IconizedContextMenu, { IconizedContextMenuOptionList, } from "../views/context_menus/IconizedContextMenu"; import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; -import * as fbEmitter from "fbemitter"; import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore"; -import { showCommunityInviteDialog } from "../../RoomInvite"; -import dis from "../../dispatcher/dispatcher"; +import { showCommunityInviteDialog, showSpaceInviteDialog } from "../../RoomInvite"; import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import ErrorDialog from "../views/dialogs/ErrorDialog"; +import InfoDialog from "../views/dialogs/InfoDialog"; import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog"; -import {UIFeature} from "../../settings/UIFeature"; +import { UIFeature } from "../../settings/UIFeature"; +import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore"; +import RoomName from "../views/elements/RoomName"; +import RoomAvatar from "../views/avatars/RoomAvatar"; +import { OpenSpaceSettingsPayload } from "../../dispatcher/payloads/OpenSpaceSettingsPayload"; +import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload"; +import SpacePublicShare from "../views/spaces/SpacePublicShare"; +import { showSpaceSettings } from "../../utils/space"; interface IProps { isMinimized: boolean; @@ -63,6 +73,8 @@ interface IState { isDarkTheme: boolean; } +const avatarSize = 32; // should match border-radius of the avatar + export default class UserMenu extends React.Component { private dispatcherRef: string; private themeWatcherRef: string; @@ -270,6 +282,7 @@ export default class UserMenu extends React.Component { if (!this.state.contextMenuPosition) return null; const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); + const userId = MatrixClientPeg.get().getUserId(); let topSection; const signupLink = getHostingLink("user-context-menu"); @@ -332,7 +345,18 @@ export default class UserMenu extends React.Component { />; } - let primaryHeader = ( + const displayName = OwnProfileStore.instance.displayName || userId; + const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); + let primaryHeader = +
{OwnProfileStore.instance.displayName} @@ -341,7 +365,8 @@ export default class UserMenu extends React.Component { {MatrixClientPeg.get().getUserId()}
- ); + ; + let primaryOptionList = ( @@ -368,7 +393,7 @@ export default class UserMenu extends React.Component { /> */} { feedbackButton } - + { }; public render() { - const avatarSize = 32; // should match border-radius of the avatar - const userId = MatrixClientPeg.get().getUserId(); const displayName = OwnProfileStore.instance.displayName || userId; const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); From 7eb27ebb598763c69f0e7c0dd0ecc2e096965a5a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 6 Jan 2021 16:04:40 +0000 Subject: [PATCH 19/94] Create space variants of some right panel cards and decorate them with the space name & avatar --- res/css/structures/_RightPanel.scss | 17 +++++++ res/css/views/rooms/_MemberInfo.scss | 1 + res/css/views/rooms/_MemberList.scss | 4 ++ src/components/structures/RightPanel.js | 23 +++++++-- src/components/views/right_panel/UserInfo.tsx | 51 +++++++++++++------ src/components/views/rooms/MemberList.js | 34 ++++++++----- .../views/rooms/ThirdPartyMemberInfo.js | 19 +++++-- .../payloads/SetRightPanelPhasePayload.ts | 2 + src/stores/RightPanelStorePhases.ts | 12 +++++ 9 files changed, 126 insertions(+), 37 deletions(-) diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 5bf0d953f3b..5515fe40600 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -160,3 +160,20 @@ limitations under the License. mask-position: center; } } + +.mx_RightPanel_scopeHeader { + margin: 24px; + text-align: center; + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + + .mx_BaseAvatar { + margin-right: 8px; + vertical-align: middle; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } +} diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index 182c280217d..3f7f83d3342 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -19,6 +19,7 @@ limitations under the License. flex-direction: column; flex: 1; overflow-y: auto; + margin-top: 8px; } .mx_MemberInfo_name { diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss index 1e3506e3715..631ddc484fd 100644 --- a/res/css/views/rooms/_MemberList.scss +++ b/res/css/views/rooms/_MemberList.scss @@ -44,6 +44,10 @@ limitations under the License. .mx_AutoHideScrollbar { flex: 1 1 0; } + + .mx_RightPanel_scopeHeader { + margin-top: -8px; + } } .mx_GroupMemberList_query, diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 41f4d837435..4b543d2c3d6 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -24,7 +24,11 @@ import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; -import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases"; +import { + RightPanelPhases, + RIGHT_PANEL_PHASES_NO_ARGS, + RIGHT_PANEL_SPACE_PHASES, +} from "../../stores/RightPanelStorePhases"; import RightPanelStore from "../../stores/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import {Action} from "../../dispatcher/actions"; @@ -80,6 +84,8 @@ export default class RightPanel extends React.Component { return RightPanelPhases.GroupMemberList; } return rps.groupPanelPhase; + } else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) { + return RightPanelPhases.SpaceMemberList; } else if (userForPanel) { // XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state // from its props and some from a store, except if the contents of the store changes @@ -100,9 +106,8 @@ export default class RightPanel extends React.Component { return rps.roomPanelPhase; } return RightPanelPhases.RoomMemberInfo; - } else { - return rps.roomPanelPhase; } + return rps.roomPanelPhase; } componentDidMount() { @@ -182,6 +187,7 @@ export default class RightPanel extends React.Component { verificationRequest: payload.verificationRequest, verificationRequestPromise: payload.verificationRequestPromise, widgetId: payload.widgetId, + space: payload.space, }); } } @@ -243,6 +249,13 @@ export default class RightPanel extends React.Component { panel = ; } break; + case RightPanelPhases.SpaceMemberList: + panel = ; + break; case RightPanelPhases.GroupMemberList: if (this.props.groupId) { @@ -255,10 +268,11 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.RoomMemberInfo: + case RightPanelPhases.SpaceMemberInfo: case RightPanelPhases.EncryptionPanel: panel = ; break; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index cdb4c43b098..5a179cdc38c 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -60,6 +60,8 @@ import QuestionDialog from "../dialogs/QuestionDialog"; import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog"; import InfoDialog from "../dialogs/InfoDialog"; import { EventType } from "matrix-js-sdk/src/@types/event"; +import RoomAvatar from "../avatars/RoomAvatar"; +import RoomName from "../elements/RoomName"; interface IDevice { deviceId: string; @@ -301,7 +303,8 @@ const UserOptionsSection: React.FC<{ member: RoomMember; isIgnored: boolean; canInvite: boolean; -}> = ({member, isIgnored, canInvite}) => { + isSpace?: boolean; +}> = ({member, isIgnored, canInvite, isSpace}) => { const cli = useContext(MatrixClientContext); let ignoreButton = null; @@ -341,7 +344,7 @@ const UserOptionsSection: React.FC<{ ); - if (member.roomId) { + if (member.roomId && !isSpace) { const onReadReceiptButton = function() { const room = cli.getRoom(member.roomId); dis.dispatch({ @@ -433,14 +436,18 @@ const UserOptionsSection: React.FC<{ ); }; -const warnSelfDemote = async () => { +const warnSelfDemote = async (isSpace) => { const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, { title: _t("Demote yourself?"), description:
- { _t("You will not be able to undo this change as you are demoting yourself, " + - "if you are the last privileged user in the room it will be impossible " + - "to regain privileges.") } + { isSpace + ? _t("You will not be able to undo this change as you are demoting yourself, " + + "if you are the last privileged user in the space it will be impossible " + + "to regain privileges.") + : _t("You will not be able to undo this change as you are demoting yourself, " + + "if you are the last privileged user in the room it will be impossible " + + "to regain privileges.") }
, button: _t("Demote"), }); @@ -716,7 +723,7 @@ const MuteToggleButton: React.FC = ({member, room, powerLevels, // if muting self, warn as it may be irreversible if (target === cli.getUserId()) { try { - if (!(await warnSelfDemote())) return; + if (!(await warnSelfDemote(room?.isSpaceRoom()))) return; } catch (e) { console.error("Failed to warn about self demotion: ", e); return; @@ -805,7 +812,7 @@ const RoomAdminToolsContainer: React.FC = ({ if (canAffectUser && me.powerLevel >= kickPowerLevel) { kickButton = ; } - if (me.powerLevel >= redactPowerLevel) { + if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) { redactButton = ( ); @@ -1084,7 +1091,7 @@ const PowerLevelEditor: React.FC<{ } else if (myUserId === target) { // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. try { - if (!(await warnSelfDemote())) return; + if (!(await warnSelfDemote(room?.isSpaceRoom()))) return; } catch (e) { console.error("Failed to warn about self demotion: ", e); } @@ -1314,12 +1321,10 @@ const BasicUserInfo: React.FC<{ if (!isRoomEncrypted) { if (!cryptoEnabled) { text = _t("This client does not support end-to-end encryption."); - } else if (room) { + } else if (room && !room.isSpaceRoom()) { text = _t("Messages in this room are not end-to-end encrypted."); - } else { - // TODO what to render for GroupMember } - } else { + } else if (!room.isSpaceRoom()) { text = _t("Messages in this room are end-to-end encrypted."); } @@ -1380,7 +1385,9 @@ const BasicUserInfo: React.FC<{ + member={member} + isSpace={room?.isSpaceRoom()} + /> { adminToolsContainer } @@ -1497,7 +1504,7 @@ interface IProps { user: Member; groupId?: string; room?: Room; - phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo; + phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo; onClose(): void; } @@ -1538,6 +1545,7 @@ const UserInfo: React.FC = ({ switch (phase) { case RightPanelPhases.RoomMemberInfo: case RightPanelPhases.GroupMemberInfo: + case RightPanelPhases.SpaceMemberInfo: content = ( = ({ } } - const header = ; + let scopeHeader; + if (room?.isSpaceRoom()) { + scopeHeader =
+ + +
; + } + + const header = + { scopeHeader } + + ; return me.powerLevel) { - canInvite = false; - } - } + const canInvite = room.canInvite(cli.getUserId()); let inviteButtonText = _t("Invite to this room"); const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat(); if (chat && chat.roomId === this.props.roomId) { inviteButtonText = _t("Invite to this community"); + } else if (room.isSpaceRoom()) { + inviteButtonText = _t("Invite to this space"); } const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); @@ -493,12 +487,26 @@ export default class MemberList extends React.Component { onSearch={ this.onSearchQueryChanged } /> ); + let previousPhase = RightPanelPhases.RoomSummary; + // We have no previousPhase for when viewing a MemberList from a Space + let scopeHeader; + if (room?.isSpaceRoom()) { + previousPhase = undefined; + scopeHeader =
+ + +
; + } + return + { scopeHeader } + { inviteButton } +
} footer={footer} onClose={this.props.onClose} - previousPhase={RightPanelPhases.RoomSummary} + previousPhase={previousPhase} >
+ + +
; + } + // We shamelessly rip off the MemberInfo styles here. return (
+ { scopeHeader }
Date: Fri, 8 Jan 2021 16:52:13 +0000 Subject: [PATCH 20/94] stash work so far --- res/css/_components.scss | 7 + res/css/structures/_SpacePanel.scss | 107 ++++ res/css/structures/_SpaceRoomView.scss | 312 +++++++++++ res/css/structures/_UserMenu.scss | 13 + .../context_menus/_IconizedContextMenu.scss | 5 + .../dialogs/_AddExistingToSpaceDialog.scss | 177 ++++++ .../views/dialogs/_SpaceSettingsDialog.scss | 55 ++ res/css/views/rooms/_RoomList.scss | 5 +- res/css/views/rooms/_RoomTile.scss | 6 + res/css/views/spaces/_SpaceBasicSettings.scss | 85 +++ res/css/views/spaces/_SpaceCreateMenu.scss | 122 +++++ res/css/views/spaces/_SpacePublicShare.scss | 57 ++ res/img/element-icons/link.svg | 3 + res/img/element-icons/plus.svg | 3 + .../element-icons/roomlist/hash-circle.svg | 7 + .../element-icons/roomlist/plus-circle.svg | 3 + res/themes/light/css/_mods.scss | 4 + src/@types/global.d.ts | 2 + src/RoomInvite.js | 9 +- src/accessibility/context_menu/MenuItem.tsx | 11 +- src/components/structures/LeftPanel.tsx | 24 +- src/components/structures/MatrixChat.tsx | 14 +- src/components/structures/RightPanel.js | 4 +- src/components/structures/RoomView.tsx | 22 +- src/components/structures/SpaceRoomView.tsx | 518 ++++++++++++++++++ src/components/structures/UserMenu.tsx | 159 +++++- .../dialogs/AddExistingToSpaceDialog.tsx | 215 ++++++++ .../views/dialogs/CreateRoomDialog.js | 7 + src/components/views/dialogs/InfoDialog.js | 6 +- src/components/views/dialogs/InviteDialog.tsx | 68 ++- .../views/dialogs/SpaceSettingsDialog.tsx | 171 ++++++ src/components/views/rooms/RoomList.tsx | 47 ++ src/components/views/rooms/RoomTile.tsx | 1 + .../views/spaces/SpaceBasicSettings.tsx | 120 ++++ .../views/spaces/SpaceCreateMenu.tsx | 170 ++++++ src/components/views/spaces/SpacePanel.tsx | 195 +++++++ .../views/spaces/SpacePublicShare.tsx | 65 +++ src/createRoom.ts | 18 +- src/dispatcher/actions.ts | 5 + .../payloads/OpenSpaceSettingsPayload.ts | 29 + src/i18n/strings/en_EN.json | 89 ++- src/stores/SpaceStore.tsx | 431 +++++++++++++++ .../notifications/SpaceNotificationState.ts | 82 +++ src/stores/room-list/RoomListStore.ts | 23 +- src/stores/room-list/SpaceWatcher.ts | 39 ++ .../room-list/filters/SpaceFilterCondition.ts | 64 +++ src/utils/space.ts | 74 +++ 47 files changed, 3576 insertions(+), 77 deletions(-) create mode 100644 res/css/structures/_SpacePanel.scss create mode 100644 res/css/structures/_SpaceRoomView.scss create mode 100644 res/css/views/dialogs/_AddExistingToSpaceDialog.scss create mode 100644 res/css/views/dialogs/_SpaceSettingsDialog.scss create mode 100644 res/css/views/spaces/_SpaceBasicSettings.scss create mode 100644 res/css/views/spaces/_SpaceCreateMenu.scss create mode 100644 res/css/views/spaces/_SpacePublicShare.scss create mode 100644 res/img/element-icons/link.svg create mode 100644 res/img/element-icons/plus.svg create mode 100644 res/img/element-icons/roomlist/hash-circle.svg create mode 100644 res/img/element-icons/roomlist/plus-circle.svg create mode 100644 src/components/structures/SpaceRoomView.tsx create mode 100644 src/components/views/dialogs/AddExistingToSpaceDialog.tsx create mode 100644 src/components/views/dialogs/SpaceSettingsDialog.tsx create mode 100644 src/components/views/spaces/SpaceBasicSettings.tsx create mode 100644 src/components/views/spaces/SpaceCreateMenu.tsx create mode 100644 src/components/views/spaces/SpacePanel.tsx create mode 100644 src/components/views/spaces/SpacePublicShare.tsx create mode 100644 src/dispatcher/payloads/OpenSpaceSettingsPayload.ts create mode 100644 src/stores/SpaceStore.tsx create mode 100644 src/stores/notifications/SpaceNotificationState.ts create mode 100644 src/stores/room-list/SpaceWatcher.ts create mode 100644 src/stores/room-list/filters/SpaceFilterCondition.ts create mode 100644 src/utils/space.ts diff --git a/res/css/_components.scss b/res/css/_components.scss index d8bc238db54..2a79bb6dfac 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -27,6 +27,8 @@ @import "./structures/_RoomView.scss"; @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; +@import "./structures/_SpacePanel.scss"; +@import "./structures/_SpaceRoomView.scss"; @import "./structures/_TabbedView.scss"; @import "./structures/_ToastContainer.scss"; @import "./structures/_UploadBar.scss"; @@ -56,6 +58,7 @@ @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; +@import "./views/dialogs/_AddExistingToSpaceDialog.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; @import "./views/dialogs/_BugReportDialog.scss"; @@ -88,6 +91,7 @@ @import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_SlashCommandHelpDialog.scss"; +@import "./views/dialogs/_SpaceSettingsDialog.scss"; @import "./views/dialogs/_TabbedIntegrationManagerDialog.scss"; @import "./views/dialogs/_TermsDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss"; @@ -230,6 +234,9 @@ @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; +@import "./views/spaces/_SpaceBasicSettings.scss"; +@import "./views/spaces/_SpaceCreateMenu.scss"; +@import "./views/spaces/_SpacePublicShare.scss"; @import "./views/terms/_InlineTermsAgreement.scss"; @import "./views/toasts/_AnalyticsToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss new file mode 100644 index 00000000000..cff448a9a29 --- /dev/null +++ b/res/css/structures/_SpacePanel.scss @@ -0,0 +1,107 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +$spacePanelWidth: 56px; // only applies in this file, used for calculations + +.mx_SpacePanel { + flex-grow: 0; + flex-shrink: 0; + background-color: $groupFilterPanel-bg-color; + + // Create another flexbox so the Panel fills the container + display: flex; + flex-direction: column; + overflow-y: auto; + + .mx_AutoHideScrollbar { + padding: 16px 12px; + } + + .mx_SpaceButton { + height: 32px; + min-height: 32px; // prevent being squished by the flexbox + width: 32px; + border-radius: 8px; + position: relative; + margin-bottom: 16px; + + &.mx_SpaceButton_active { + &::before { + position: absolute; + content: ''; + height: 32px; + width: 4px; + top: 0; + left: -12px; + background-color: $accent-color; + border-radius: 0 4px 4px 0; + } + } + + &.mx_SpaceButton_home, &.mx_SpaceButton_new { + &::after { + position: absolute; + content: ''; + width: 32px; + height: 32px; + top: 0; + left: 0; + mask-position: center; + mask-repeat: no-repeat; + } + } + + &.mx_SpaceButton_home { + background-color: #ffffff; + + &::after { + background-color: #3F3D3D; + mask-image: url('$(res)/img/element-icons/home.svg'); + mask-size: 18px; + } + } + + &.mx_SpaceButton_new { + background-color: $accent-color; + transition: all .1s ease-in-out; // TODO transition + + &::after { + background-color: #ffffff; + mask-image: url('$(res)/img/element-icons/plus.svg'); + mask-size: 14px; + transition: all .2s ease-in-out; // TODO transition + } + } + + &.mx_SpaceButton_newCancel { + background-color: $icon-button-color; + + &::after { + transform: rotate(45deg); + } + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + + .mx_NotificationBadge { + position: absolute; + right: -4px; + top: -4px; + } + } +} diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss new file mode 100644 index 00000000000..acc2e35be76 --- /dev/null +++ b/res/css/structures/_SpaceRoomView.scss @@ -0,0 +1,312 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +$SpaceRoomViewInnerWidth: 428px; + +.mx_SpaceRoomView { + .mx_MainSplit > div:first-child { + padding: 80px 60px; + flex-grow: 1; + + h1 { + margin: 0; + font-size: $font-24px; + font-weight: $font-semi-bold; + color: $primary-fg-color; + width: max-content; + } + + .mx_SpaceRoomView_description { + font-size: $font-15px; + color: $secondary-fg-color; + margin-top: 12px; + margin-bottom: 24px; + } + + .mx_SpaceRoomView_buttons { + display: block; + margin-top: 44px; + width: $SpaceRoomViewInnerWidth; + text-align: right; // button alignment right + + .mx_FormButton { + padding: 8px 22px; + margin-left: 16px; + } + } + + .mx_Field { + max-width: $SpaceRoomViewInnerWidth; + + & + .mx_Field { + margin-top: 28px; + } + } + + .mx_SpaceRoomView_errorText { + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + color: $notice-primary-color; + margin-bottom: 28px; + } + + .mx_AccessibleButton_disabled { + cursor: not-allowed; + } + } + + .mx_SpaceRoomView_landing { + .mx_BaseAvatar { + > img { + border-radius: 12px; + } + } + + .mx_SpaceRoomView_landing_name { + margin: 20px 0 16px; + font-size: $font-15px; + color: $secondary-fg-color; + + > span { + display: inline-block; + } + + .mx_SpaceRoomView_landing_memberCount { + position: relative; + margin-left: 24px; + padding: 0 0 0 28px; + line-height: $font-24px; + + &::before { + position: absolute; + content: ''; + width: 24px; + height: 24px; + top: 0; + left: 0; + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + background-color: $accent-color; + mask-image: url('$(res)/img/element-icons/community-members.svg'); + } + } + } + + .mx_SpaceRoomView_landing_topic { + font-size: $font-15px; + } + + .mx_SpaceRoomView_landing_joinButtons { + margin-top: 24px; + + .mx_FormButton { + padding: 8px 22px; + } + } + + .mx_SpaceRoomView_landing_adminButtons { + margin-top: 32px; + + .mx_AccessibleButton { + position: relative; + width: 160px; + height: 124px; + box-sizing: border-box; + padding: 72px 16px 0; + border-radius: 12px; + border: 1px solid rgba(141, 151, 165, 0.3); + margin-right: 28px; + margin-bottom: 28px; + font-size: $font-14px; + display: inline-block; + vertical-align: bottom; + + &:last-child { + margin-right: 0; + } + + &:hover { + background-color: rgba(141, 151, 165, 0.1); + border-color: rgba(141, 151, 165, 0.1); + } + + &::before, &::after { + position: absolute; + content: ""; + left: 16px; + top: 16px; + height: 40px; + width: 40px; + border-radius: 20px; + } + + &::after { + mask-position: center; + mask-size: 30px; + mask-repeat: no-repeat; + background: #ffffff; // white icon fill + } + + &.mx_SpaceRoomView_landing_inviteButton { + &::before { + background-color: $accent-color; + } + + &::after { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + + &.mx_SpaceRoomView_landing_addButton { + &::before { + background-color: #AC3BA8; + } + + &::after { + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); + } + } + + &.mx_SpaceRoomView_landing_createButton { + &::before { + background-color: #368BD6; + } + + &::after { + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); + } + } + + &.mx_SpaceRoomView_landing_settingsButton { + &::before { + background-color: #5C56F5; + } + + &::after { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + } + } + } + } + + .mx_SpaceRoomView_privateScope { + .mx_RadioButton { + width: $SpaceRoomViewInnerWidth; + border-radius: 8px; + border: 1px solid $input-darker-bg-color; + padding: 16px 16px 16px 72px; + margin-top: 36px; + cursor: pointer; + box-sizing: border-box; + position: relative; + + > div:first-of-type { + // hide radio dot + display: none; + } + + .mx_RadioButton_content { + margin: 0; + + > h3 { + margin: 0 0 4px; + font-size: $font-15px; + font-weight: $font-semi-bold; + line-height: $font-18px; + } + + > div { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + } + } + + &::before { + content: ""; + position: absolute; + height: 32px; + width: 32px; + top: 24px; + left: 20px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + } + + .mx_RadioButton_checked { + border-color: $accent-color; + + .mx_RadioButton_content { + > div { + color: $primary-fg-color; + } + } + + &::before { + background-color: $accent-color; + } + } + + .mx_SpaceRoomView_privateScope_justMeButton::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + .mx_SpaceRoomView_privateScope_meAndMyTeammatesButton::before { + mask-image: url('$(res)/img/element-icons/community-members.svg'); + } + } + + .mx_SpaceRoomView_inviteTeammates { + .mx_SpaceRoomView_inviteTeammates_buttons { + color: $secondary-fg-color; + margin-top: 28px; + + .mx_AccessibleButton { + position: relative; + display: inline-block; + padding-left: 32px; + line-height: 24px; // to center icons + + &::before { + content: ""; + position: absolute; + height: 24px; + width: 24px; + top: 0; + left: 0; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + & + .mx_AccessibleButton { + margin-left: 32px; + } + } + + .mx_SpaceRoomView_inviteTeammates_inviteDialogButton::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + } +} diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index f6fa998fd40..0953419dc87 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -216,6 +216,15 @@ limitations under the License. } } + .mx_UserMenu_contextMenu_spaceAvatar { + margin-right: 8px; + vertical-align: middle; + } + + img.mx_UserMenu_contextMenu_spaceAvatar, .mx_UserMenu_contextMenu_spaceAvatar > img { + border-radius: 8px; + } + .mx_UserMenu_contextMenu_userAvatar { margin-right: 8px; } @@ -336,3 +345,7 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/invite.svg'); } } + +.mx_UserMenu_sharePublicSpace { + margin: 0; +} diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss index d911ac6dfea..204435995f1 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.scss +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -75,6 +75,11 @@ limitations under the License. background-color: $menu-selected-color; } + &.mx_AccessibleButton_disabled { + opacity: 0.5; + cursor: not-allowed; + } + img, .mx_IconizedContextMenu_icon { // icons width: 16px; min-width: 16px; diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss new file mode 100644 index 00000000000..eb62855a317 --- /dev/null +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -0,0 +1,177 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_AddExistingToSpaceDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_AddExistingToSpaceDialog { + width: 480px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + + .mx_Dialog_title { + display: flex; + + .mx_BaseAvatar { + margin-right: 16px; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + + > div { + > h1 { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + margin: 0; + } + + .mx_AddExistingToSpaceDialog_onlySpace { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + } + } + + .mx_Dropdown_input { + border: none; + + > .mx_Dropdown_option { + padding-left: 0; + flex: unset; + height: unset; + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + + .mx_BaseAvatar { + display: none; + } + } + + .mx_Dropdown_menu { + .mx_AddExistingToSpaceDialog_dropdownOptionActive { + color: $accent-color; + padding-right: 32px; + position: relative; + + &::before { + content: ''; + width: 20px; + height: 20px; + top: 8px; + right: 0; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $accent-color; + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + } + } + } + } + + .mx_SearchBox { + margin: 0; + } + + .mx_AddExistingToSpaceDialog_errorText { + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + color: $notice-primary-color; + margin-bottom: 28px; + } + + .mx_AddExistingToSpaceDialog_content { + margin-top: 24px; + } + + .mx_AddExistingToSpaceDialog_section { + > h3 { + margin: 0; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_AddExistingToSpaceDialog_entry { + display: flex; + margin-top: 12px; + + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_AddExistingToSpaceDialog_entry_name { + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + } + + .mx_FormButton { + min-width: 92px; + font-weight: normal; + box-sizing: border-box; + } + } + } + + .mx_AddExistingToSpaceDialog_section_spaces { + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_AddExistingToSpaceDialog_footer { + display: flex; + margin-top: 32px; + + > span { + flex-grow: 1; + font-size: $font-12px; + line-height: $font-15px; + + > * { + vertical-align: middle; + } + } + + .mx_AccessibleButton { + display: inline-block; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + } + + .mx_FormButton { + padding: 8px 22px; + } +} diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss new file mode 100644 index 00000000000..6b9f3a0ceef --- /dev/null +++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss @@ -0,0 +1,55 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SpaceSettingsDialog { + width: 480px; + color: $primary-fg-color; + + .mx_SpaceSettings_errorText { + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + color: $notice-primary-color; + margin-bottom: 28px; + } + + .mx_ToggleSwitch { + display: inline-block; + vertical-align: middle; + margin-left: 16px; + } + + .mx_AccessibleButton_kind_danger { + margin-top: 28px; + } + + .mx_SpaceSettingsDialog_buttons { + display: flex; + margin-top: 64px; + + .mx_AccessibleButton { + display: inline-block; + } + + .mx_AccessibleButton_kind_link { + margin-left: auto; + } + } + + .mx_FormButton { + padding: 8px 22px; + } +} diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index b7759d265fb..c0d4fce3348 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -19,7 +19,10 @@ limitations under the License. } .mx_RoomList_iconPlus::before { - mask-image: url('$(res)/img/element-icons/roomlist/plus.svg'); + mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg'); +} +.mx_RoomList_iconHash::before { + mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg'); } .mx_RoomList_iconExplore::before { mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 8eca3f1efae..40a270f37ce 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -160,6 +160,12 @@ limitations under the License. margin-right: 0; } } + + &.mx_RoomTile_space { + .mx_BaseAvatar_image { + border-radius: 8px; + } + } } // We use these both in context menus and the room tiles diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss new file mode 100644 index 00000000000..cf64018dc06 --- /dev/null +++ b/res/css/views/spaces/_SpaceBasicSettings.scss @@ -0,0 +1,85 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SpaceBasicSettings { + .mx_Field { + margin: 32px 0; + } + + .mx_SpaceBasicSettings_avatarContainer { + display: flex; + margin-top: 24px; + + .mx_SpaceBasicSettings_avatar { + position: relative; + height: 80px; + width: 80px; + background-color: $tertiary-fg-color; + border-radius: 16px; + } + + img.mx_SpaceBasicSettings_avatar { + width: 80px; + height: 80px; + object-fit: cover; + border-radius: 16px; + } + + // only show it when the button is a div and not an img (has avatar) + div.mx_SpaceBasicSettings_avatar { + cursor: pointer; + + &::before { + content: ""; + position: absolute; + height: 80px; + width: 80px; + top: 0; + left: 0; + background-color: #ffffff; // white icon fill + mask-repeat: no-repeat; + mask-position: center; + mask-size: 20px; + mask-image: url('$(res)/img/element-icons/camera.svg'); + } + } + + > input[type="file"] { + display: none; + } + + > .mx_AccessibleButton_kind_link { + display: inline-block; + padding: 0; + margin: auto 16px; + } + + > .mx_SpaceBasicSettings_avatar_remove { + color: $notice-primary-color; + } + } + + .mx_FormButton { + padding: 8px 22px; + margin-left: auto; + display: block; + width: min-content; + } + + .mx_AccessibleButton_disabled { + cursor: not-allowed; + } +} diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss new file mode 100644 index 00000000000..bbe39bbf0c1 --- /dev/null +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -0,0 +1,122 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SpaceCreateMenu_wrapper { + // background blur everything except SpacePanel + .mx_ContextualMenu_background { + background-color: $dialog-backdrop-color; + opacity: 0.6; + left: $spacePanelWidth; + } + + .mx_ContextualMenu { + padding: 24px; + width: 480px; + box-sizing: border-box; + + > h2 { + font-weight: $font-semi-bold; + font-size: $font-18px; + margin-top: 4px; + } + + > p { + font-size: $font-15px; + color: $secondary-fg-color; + margin: 0; + } + + .mx_SpaceCreateMenuType { + position: relative; + padding: 16px 32px 16px 72px; + width: 432px; + box-sizing: border-box; + border-radius: 8px; + border: 1px solid $input-darker-bg-color; + font-size: $font-15px; + margin: 20px 0; + + > h3 { + font-weight: $font-semi-bold; + margin: 0 0 4px; + } + + &::before { + position: absolute; + content: ''; + width: 32px; + height: 32px; + top: 24px; + left: 20px; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 32px; + background-color: $tertiary-fg-color; + } + + &:hover { + border-color: $accent-color; + + &::before { + background-color: $accent-color; + } + } + } + + .mx_SpaceCreateMenuType_community::before { + mask-image: url('$(res)/img/globe.svg'); + mask-size: 26px; + } + .mx_SpaceCreateMenuType_workspace::before { + mask-image: url('$(res)/img/element-icons/community-members.svg'); + } + + .mx_SpaceCreateMenu_back { + width: 28px; + height: 28px; + position: relative; + background-color: $theme-button-bg-color; + border-radius: 14px; + margin-bottom: 12px; + + &::before { + content: ""; + position: absolute; + height: 28px; + width: 28px; + top: 0; + left: 0; + background-color: $muted-fg-color; + transform: rotate(90deg); + mask-repeat: no-repeat; + mask-position: 2px 3px; + mask-size: 24px; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + } + + .mx_FormButton { + padding: 8px 22px; + margin-left: auto; + display: block; + width: min-content; + } + + .mx_AccessibleButton_disabled { + cursor: not-allowed; + } + } +} diff --git a/res/css/views/spaces/_SpacePublicShare.scss b/res/css/views/spaces/_SpacePublicShare.scss new file mode 100644 index 00000000000..08c6d661099 --- /dev/null +++ b/res/css/views/spaces/_SpacePublicShare.scss @@ -0,0 +1,57 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SpacePublicShare { + .mx_AccessibleButton { + background: rgba(244, 246, 250, 0.91); + border: 1px solid $input-darker-bg-color; + box-sizing: border-box; + border-radius: 8px; + padding: 12px 24px 12px 52px; + margin-top: 16px; + width: $SpaceRoomViewInnerWidth; + font-size: $font-15px; + line-height: $font-24px; + position: relative; + display: flex; + + > span { + color: #368BD6; + margin-left: auto; + } + + &::before { + content: ""; + position: absolute; + width: 30px; + height: 30px; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + background: $muted-fg-color; + left: 12px; + top: 9px; + } + + &.mx_SpacePublicShare_shareButton::before { + mask-image: url('$(res)/img/element-icons/link.svg'); + } + + &.mx_SpacePublicShare_inviteButton::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } +} diff --git a/res/img/element-icons/link.svg b/res/img/element-icons/link.svg new file mode 100644 index 00000000000..ab3d54b838e --- /dev/null +++ b/res/img/element-icons/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/plus.svg b/res/img/element-icons/plus.svg new file mode 100644 index 00000000000..49e0669ccb6 --- /dev/null +++ b/res/img/element-icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/hash-circle.svg b/res/img/element-icons/roomlist/hash-circle.svg new file mode 100644 index 00000000000..924b22cf329 --- /dev/null +++ b/res/img/element-icons/roomlist/hash-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/roomlist/plus-circle.svg b/res/img/element-icons/roomlist/plus-circle.svg new file mode 100644 index 00000000000..251ded225cf --- /dev/null +++ b/res/img/element-icons/roomlist/plus-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss index 30aaeedf8fe..fbca58dfb1e 100644 --- a/res/themes/light/css/_mods.scss +++ b/res/themes/light/css/_mods.scss @@ -16,6 +16,10 @@ backdrop-filter: blur($groupFilterPanel-background-blur-amount); } + .mx_SpacePanel { + backdrop-filter: blur($groupFilterPanel-background-blur-amount); + } + .mx_LeftPanel .mx_LeftPanel_roomListContainer { backdrop-filter: blur($roomlist-background-blur-amount); } diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 741798761f2..e45c6d83592 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -36,6 +36,7 @@ import {Analytics} from "../Analytics"; import CountlyAnalytics from "../CountlyAnalytics"; import UserActivity from "../UserActivity"; import {ModalWidgetStore} from "../stores/ModalWidgetStore"; +import {SpaceStoreClass} from "../stores/SpaceStore"; declare global { interface Window { @@ -64,6 +65,7 @@ declare global { mxCountlyAnalytics: typeof CountlyAnalytics; mxUserActivity: UserActivity; mxModalWidgetStore: ModalWidgetStore; + mxSpaceStore: SpaceStoreClass; } interface Document { diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 06d3fb04e88..728ae11e794 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter'; import Modal from './Modal'; import * as sdk from './'; import { _t } from './languageHandler'; -import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; +import InviteDialog, {KIND_DM, KIND_INVITE, KIND_SPACE_INVITE} from "./components/views/dialogs/InviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; @@ -75,6 +75,13 @@ export function showCommunityInviteDialog(communityId) { } } +export const showSpaceInviteDialog = (roomId) => { + Modal.createTrackedDialog("Invite Users", "Space", InviteDialog, { + kind: KIND_SPACE_INVITE, + roomId, + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); +}; + /** * Checks if the given MatrixEvent is a valid 3rd party user invite. * @param {MatrixEvent} event The event to check diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx index 0bb169abf82..9a7c1d1f0ae 100644 --- a/src/accessibility/context_menu/MenuItem.tsx +++ b/src/accessibility/context_menu/MenuItem.tsx @@ -19,14 +19,23 @@ limitations under the License. import React from "react"; import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; interface IProps extends React.ComponentProps { label?: string; + tooltip?: string; } // Semantic component for representing a role=menuitem -export const MenuItem: React.FC = ({children, label, ...props}) => { +export const MenuItem: React.FC = ({children, label, tooltip, ...props}) => { const ariaLabel = props["aria-label"] || label; + + if (tooltip) { + return + { children } + ; + } + return ( { children } diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 4445ff3ff84..84967f89e46 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -39,6 +39,7 @@ import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; +import SpacePanel from "../views/spaces/SpacePanel"; interface IProps { isMinimized: boolean; @@ -388,12 +389,19 @@ export default class LeftPanel extends React.Component { } public render(): React.ReactNode { - const groupFilterPanel = !this.state.showGroupFilterPanel ? null : ( -
- - {SettingsStore.getValue("feature_custom_tags") ? : null} -
- ); + let leftLeftPanel; + // Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now + // ignore it and force the rendering of SpacePanel if that Labs flag is enabled. + if (SettingsStore.getValue("feature_spaces")) { + leftLeftPanel = ; + } else if (this.state.showGroupFilterPanel) { + leftLeftPanel = ( +
+ + {SettingsStore.getValue("feature_custom_tags") ? : null} +
+ ); + } const roomList = { const containerClasses = classNames({ "mx_LeftPanel": true, - "mx_LeftPanel_hasGroupFilterPanel": !!groupFilterPanel, + "mx_LeftPanel_hasGroupFilterPanel": !!leftLeftPanel, "mx_LeftPanel_minimized": this.props.isMinimized, }); @@ -417,7 +425,7 @@ export default class LeftPanel extends React.Component { return (
- {groupFilterPanel} + {leftLeftPanel}