Merge pull request #5902 from matrix-org/t3chguy/fix/17020
Show invites to spaces at the top of the space panelpull/21833/head
commit
ba9cfa0942
|
@ -548,6 +548,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
|
||||||
let explorePrompt: JSX.Element;
|
let explorePrompt: JSX.Element;
|
||||||
if (!this.props.isMinimized) {
|
if (!this.props.isMinimized) {
|
||||||
if (this.state.isNameFiltering) {
|
if (this.state.isNameFiltering) {
|
||||||
|
@ -568,21 +571,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
|
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.props.activeSpace) {
|
} else if (
|
||||||
|
this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join"
|
||||||
|
) {
|
||||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||||
<div>{ _t("Quick actions") }</div>
|
<div>{ _t("Quick actions") }</div>
|
||||||
{ this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && <AccessibleButton
|
{ this.props.activeSpace.canInvite(userId) && <AccessibleButton
|
||||||
className="mx_RoomList_explorePrompt_spaceInvite"
|
className="mx_RoomList_explorePrompt_spaceInvite"
|
||||||
onClick={this.onSpaceInviteClick}
|
onClick={this.onSpaceInviteClick}
|
||||||
>
|
>
|
||||||
{_t("Invite people")}
|
{_t("Invite people")}
|
||||||
</AccessibleButton> }
|
</AccessibleButton> }
|
||||||
<AccessibleButton
|
{ this.props.activeSpace.getMyMembership() === "join" && <AccessibleButton
|
||||||
className="mx_RoomList_explorePrompt_spaceExplore"
|
className="mx_RoomList_explorePrompt_spaceExplore"
|
||||||
onClick={this.onExplore}
|
onClick={this.onExplore}
|
||||||
>
|
>
|
||||||
{_t("Explore rooms")}
|
{_t("Explore rooms")}
|
||||||
</AccessibleButton>
|
</AccessibleButton> }
|
||||||
</div>;
|
</div>;
|
||||||
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
||||||
const unfilteredLists = RoomListStore.instance.unfilteredLists
|
const unfilteredLists = RoomListStore.instance.unfilteredLists
|
||||||
|
|
|
@ -25,7 +25,12 @@ import SpaceCreateMenu from "./SpaceCreateMenu";
|
||||||
import {SpaceItem} from "./SpaceTreeLevel";
|
import {SpaceItem} from "./SpaceTreeLevel";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore";
|
import SpaceStore, {
|
||||||
|
HOME_SPACE,
|
||||||
|
UPDATE_INVITED_SPACES,
|
||||||
|
UPDATE_SELECTED_SPACE,
|
||||||
|
UPDATE_TOP_LEVEL_SPACES,
|
||||||
|
} from "../../../stores/SpaceStore";
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
||||||
import NotificationBadge from "../rooms/NotificationBadge";
|
import NotificationBadge from "../rooms/NotificationBadge";
|
||||||
|
@ -105,19 +110,21 @@ const SpaceButton: React.FC<IButtonProps> = ({
|
||||||
</li>;
|
</li>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useSpaces = (): [Room[], Room | null] => {
|
const useSpaces = (): [Room[], Room[], Room | null] => {
|
||||||
|
const [invites, setInvites] = useState<Room[]>(SpaceStore.instance.invitedSpaces);
|
||||||
|
useEventEmitter(SpaceStore.instance, UPDATE_INVITED_SPACES, setInvites);
|
||||||
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
|
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
|
||||||
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
|
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
|
||||||
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
|
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
|
||||||
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
|
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
|
||||||
return [spaces, activeSpace];
|
return [invites, spaces, activeSpace];
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpacePanel = () => {
|
const SpacePanel = () => {
|
||||||
// We don't need the handle as we position the menu in a constant location
|
// We don't need the handle as we position the menu in a constant location
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
|
||||||
const [spaces, activeSpace] = useSpaces();
|
const [invites, spaces, activeSpace] = useSpaces();
|
||||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||||
|
|
||||||
const newClasses = classNames("mx_SpaceButton_new", {
|
const newClasses = classNames("mx_SpaceButton_new", {
|
||||||
|
@ -209,6 +216,13 @@ const SpacePanel = () => {
|
||||||
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
||||||
isNarrow={isPanelCollapsed}
|
isNarrow={isPanelCollapsed}
|
||||||
/>
|
/>
|
||||||
|
{ invites.map(s => <SpaceItem
|
||||||
|
key={s.roomId}
|
||||||
|
space={s}
|
||||||
|
activeSpaces={activeSpaces}
|
||||||
|
isPanelCollapsed={isPanelCollapsed}
|
||||||
|
onExpand={() => setPanelCollapsed(false)}
|
||||||
|
/>) }
|
||||||
{ spaces.map(s => <SpaceItem
|
{ spaces.map(s => <SpaceItem
|
||||||
key={s.roomId}
|
key={s.roomId}
|
||||||
space={s}
|
space={s}
|
||||||
|
|
|
@ -45,6 +45,8 @@ import RoomViewStore from "../../../stores/RoomViewStore";
|
||||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||||
|
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
||||||
|
import {NotificationColor} from "../../../stores/notifications/NotificationColor";
|
||||||
|
|
||||||
interface IItemProps {
|
interface IItemProps {
|
||||||
space?: Room;
|
space?: Room;
|
||||||
|
@ -67,7 +69,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
collapsed: !props.isNested, // default to collapsed for root items
|
collapsed: !props.isNested, // default to collapsed for root items
|
||||||
contextMenuPosition: null,
|
contextMenuPosition: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -83,6 +85,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onContextMenu = (ev: React.MouseEvent) => {
|
private onContextMenu = (ev: React.MouseEvent) => {
|
||||||
|
if (this.props.space.getMyMembership() !== "join") return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -185,6 +188,8 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderContextMenu(): React.ReactElement {
|
private renderContextMenu(): React.ReactElement {
|
||||||
|
if (this.props.space.getMyMembership() !== "join") return null;
|
||||||
|
|
||||||
let contextMenu = null;
|
let contextMenu = null;
|
||||||
if (this.state.contextMenuPosition) {
|
if (this.state.contextMenuPosition) {
|
||||||
const userId = this.context.getUserId();
|
const userId = this.context.getUserId();
|
||||||
|
@ -300,7 +305,9 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
|
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
|
||||||
mx_SpaceButton_narrow: isNarrow,
|
mx_SpaceButton_narrow: isNarrow,
|
||||||
});
|
});
|
||||||
const notificationState = SpaceStore.instance.getNotificationState(space.roomId);
|
const notificationState = space.getMyMembership() === "invite"
|
||||||
|
? StaticNotificationState.forSymbol("!", NotificationColor.Red)
|
||||||
|
: SpaceStore.instance.getNotificationState(space.roomId);
|
||||||
|
|
||||||
let childItems;
|
let childItems;
|
||||||
if (childSpaces && !collapsed) {
|
if (childSpaces && !collapsed) {
|
||||||
|
|
|
@ -46,6 +46,7 @@ export const HOME_SPACE = Symbol("home-space");
|
||||||
export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
|
export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
|
||||||
|
|
||||||
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
||||||
|
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
|
||||||
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
|
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
|
||||||
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
|
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
|
||||||
|
|
||||||
|
@ -93,6 +94,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// The space currently selected in the Space Panel - if null then `Home` is selected
|
// The space currently selected in the Space Panel - if null then `Home` is selected
|
||||||
private _activeSpace?: Room = null;
|
private _activeSpace?: Room = null;
|
||||||
private _suggestedRooms: ISpaceSummaryRoom[] = [];
|
private _suggestedRooms: ISpaceSummaryRoom[] = [];
|
||||||
|
private _invitedSpaces = new Set<Room>();
|
||||||
|
|
||||||
|
public get invitedSpaces(): Room[] {
|
||||||
|
return Array.from(this._invitedSpaces);
|
||||||
|
}
|
||||||
|
|
||||||
public get spacePanelSpaces(): Room[] {
|
public get spacePanelSpaces(): Room[] {
|
||||||
return this.rootSpaces;
|
return this.rootSpaces;
|
||||||
|
@ -117,7 +123,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// view last selected room from space
|
// view last selected room from space
|
||||||
const roomId = window.localStorage.getItem(getSpaceContextKey(this.activeSpace));
|
const roomId = window.localStorage.getItem(getSpaceContextKey(this.activeSpace));
|
||||||
|
|
||||||
if (roomId && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join") {
|
// if the space being selected is an invite then always view that invite
|
||||||
|
// else if the last viewed room in this space is joined then view that
|
||||||
|
// else view space home or home depending on what is being clicked on
|
||||||
|
if (space?.getMyMembership !== "invite" &&
|
||||||
|
this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join"
|
||||||
|
) {
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: "view_room",
|
action: "view_room",
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
|
@ -214,25 +225,27 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return sortBy(parents, r => r.roomId)?.[0] || null;
|
return sortBy(parents, r => r.roomId)?.[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSpaces = () => {
|
|
||||||
return this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "join");
|
|
||||||
};
|
|
||||||
|
|
||||||
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
|
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
|
||||||
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
|
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
|
||||||
};
|
};
|
||||||
|
|
||||||
private rebuild = throttle(() => {
|
private rebuild = throttle(() => {
|
||||||
// get all most-upgraded rooms & spaces except spaces which have been left (historical)
|
const [visibleSpaces, visibleRooms] = partitionSpacesAndRooms(this.matrixClient.getVisibleRooms());
|
||||||
const visibleRooms = this.matrixClient.getVisibleRooms().filter(r => {
|
const [joinedSpaces, invitedSpaces] = visibleSpaces.reduce((arr, s) => {
|
||||||
return !r.isSpaceRoom() || r.getMyMembership() === "join";
|
if (s.getMyMembership() === "join") {
|
||||||
});
|
arr[0].push(s);
|
||||||
|
} else if (s.getMyMembership() === "invite") {
|
||||||
|
arr[1].push(s);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [[], []]);
|
||||||
|
|
||||||
const unseenChildren = new Set<Room>(visibleRooms);
|
// exclude invited spaces from unseenChildren as they will be forcibly shown at the top level of the treeview
|
||||||
|
const unseenChildren = new Set<Room>([...visibleRooms, ...joinedSpaces]);
|
||||||
const backrefs = new EnhancedMap<string, Set<string>>();
|
const backrefs = new EnhancedMap<string, Set<string>>();
|
||||||
|
|
||||||
// Sort spaces by room ID to force the cycle breaking to be deterministic
|
// Sort spaces by room ID to force the cycle breaking to be deterministic
|
||||||
const spaces = sortBy(visibleRooms.filter(r => r.isSpaceRoom()), space => space.roomId);
|
const spaces = sortBy(joinedSpaces, space => space.roomId);
|
||||||
|
|
||||||
// TODO handle cleaning up links when a Space is removed
|
// TODO handle cleaning up links when a Space is removed
|
||||||
spaces.forEach(space => {
|
spaces.forEach(space => {
|
||||||
|
@ -296,6 +309,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
this.onRoomsUpdate(); // TODO only do this if a change has happened
|
this.onRoomsUpdate(); // TODO only do this if a change has happened
|
||||||
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
|
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
|
||||||
|
|
||||||
|
// build initial state of invited spaces as we would have missed the emitted events about the room at launch
|
||||||
|
this._invitedSpaces = new Set(invitedSpaces);
|
||||||
|
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
||||||
}, 100, {trailing: true, leading: true});
|
}, 100, {trailing: true, leading: true});
|
||||||
|
|
||||||
onSpaceUpdate = () => {
|
onSpaceUpdate = () => {
|
||||||
|
@ -303,6 +320,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private showInHomeSpace = (room: Room) => {
|
private showInHomeSpace = (room: Room) => {
|
||||||
|
if (room.isSpaceRoom()) return false;
|
||||||
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|
||||||
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
|
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
|
||||||
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
|
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
|
||||||
|
@ -333,8 +351,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const oldFilteredRooms = this.spaceFilteredRooms;
|
const oldFilteredRooms = this.spaceFilteredRooms;
|
||||||
this.spaceFilteredRooms = new Map();
|
this.spaceFilteredRooms = new Map();
|
||||||
|
|
||||||
// put all invites (rooms & spaces) in the Home Space
|
// put all room invites in the Home Space
|
||||||
const invites = this.matrixClient.getRooms().filter(r => r.getMyMembership() === "invite");
|
const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
|
||||||
this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId)));
|
this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId)));
|
||||||
|
|
||||||
visibleRooms.forEach(room => {
|
visibleRooms.forEach(room => {
|
||||||
|
@ -391,8 +409,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
});
|
});
|
||||||
}, 100, {trailing: true, leading: true});
|
}, 100, {trailing: true, leading: true});
|
||||||
|
|
||||||
private onRoom = (room: Room) => {
|
private onRoom = (room: Room, membership?: string, oldMembership?: string) => {
|
||||||
if (room?.isSpaceRoom()) {
|
if ((membership || room.getMyMembership()) === "invite") {
|
||||||
|
this._invitedSpaces.add(room);
|
||||||
|
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
||||||
|
} else if (oldMembership === "invite") {
|
||||||
|
this._invitedSpaces.delete(room);
|
||||||
|
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
||||||
|
} else if (room?.isSpaceRoom()) {
|
||||||
this.onSpaceUpdate();
|
this.onSpaceUpdate();
|
||||||
this.emit(room.roomId);
|
this.emit(room.roomId);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -599,11 +599,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
private getPlausibleRooms(): Room[] {
|
private getPlausibleRooms(): Room[] {
|
||||||
if (!this.matrixClient) return [];
|
if (!this.matrixClient) return [];
|
||||||
|
|
||||||
let rooms = [
|
let rooms = this.matrixClient.getVisibleRooms().filter(r => VisibilityProvider.instance.isRoomVisible(r));
|
||||||
...this.matrixClient.getVisibleRooms(),
|
|
||||||
// also show space invites in the room list
|
|
||||||
...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"),
|
|
||||||
].filter(r => VisibilityProvider.instance.isRoomVisible(r));
|
|
||||||
|
|
||||||
if (this.prefilterConditions.length > 0) {
|
if (this.prefilterConditions.length > 0) {
|
||||||
rooms = rooms.filter(r => {
|
rooms = rooms.filter(r => {
|
||||||
|
|
Loading…
Reference in New Issue