Merge branch 'develop' into andybalaam/stas-demydiuk-membership-type3
This commit is contained in:
commit
d7bdbee8d2
124 changed files with 2399 additions and 1052 deletions
|
@ -20,7 +20,7 @@ import { Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/matrix
|
|||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import SmartMarker from "../location/SmartMarker";
|
||||
import { SmartMarker } from "../location";
|
||||
|
||||
interface Props {
|
||||
map: maplibregl.Map;
|
||||
|
|
|
@ -36,7 +36,7 @@ import MapFallback from "../location/MapFallback";
|
|||
import { MapError } from "../location/MapError";
|
||||
import { LocationShareError } from "../../../utils/location";
|
||||
|
||||
interface IProps {
|
||||
export interface IProps {
|
||||
roomId: Room["roomId"];
|
||||
matrixClient: MatrixClient;
|
||||
// open the map centered on this beacon's location
|
||||
|
|
31
src/components/views/beacon/index.tsx
Normal file
31
src/components/views/beacon/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Copyright 2024 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.
|
||||
*/
|
||||
|
||||
// Exports beacon components which touch maplibre-gs wrapped in React Suspense to enable code splitting
|
||||
|
||||
import React, { ComponentProps, lazy, Suspense } from "react";
|
||||
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
const BeaconViewDialogComponent = lazy(() => import("./BeaconViewDialog"));
|
||||
|
||||
export function BeaconViewDialog(props: ComponentProps<typeof BeaconViewDialogComponent>): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<BeaconViewDialogComponent {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
|
@ -30,7 +30,7 @@ import { NotificationLevel } from "../../../stores/notifications/NotificationLev
|
|||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { clearRoomNotification } from "../../../utils/notifications";
|
||||
import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications";
|
||||
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuCheckbox,
|
||||
|
@ -45,13 +45,60 @@ import { useSettingValue } from "../../../hooks/useSettings";
|
|||
|
||||
export interface RoomGeneralContextMenuProps extends IContextMenuProps {
|
||||
room: Room;
|
||||
/**
|
||||
* Called when the 'favourite' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostFavoriteClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'low priority' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostLowPriorityClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'invite' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostInviteClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'copy link' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostCopyLinkClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'settings' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostSettingsClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'forget room' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostForgetClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'leave' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostLeaveClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'mark as read' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostMarkAsReadClick?: (event: ButtonEvent) => void;
|
||||
/**
|
||||
* Called when the 'mark as unread' option is selected, after the menu has processed
|
||||
* the mouse or keyboard event.
|
||||
* @param event The event that caused the option to be selected.
|
||||
*/
|
||||
onPostMarkAsUnreadClick?: (event: ButtonEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,6 +114,8 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
|
|||
onPostSettingsClick,
|
||||
onPostLeaveClick,
|
||||
onPostForgetClick,
|
||||
onPostMarkAsReadClick,
|
||||
onPostMarkAsUnreadClick,
|
||||
...props
|
||||
}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
@ -213,18 +262,33 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
|
|||
}
|
||||
|
||||
const { level } = useUnreadNotifications(room);
|
||||
const markAsReadOption: JSX.Element | null =
|
||||
level > NotificationLevel.None ? (
|
||||
<IconizedContextMenuCheckbox
|
||||
onClick={() => {
|
||||
clearRoomNotification(room, cli);
|
||||
onFinished?.();
|
||||
}}
|
||||
active={false}
|
||||
label={_t("room|context_menu|mark_read")}
|
||||
iconClassName="mx_RoomGeneralContextMenu_iconMarkAsRead"
|
||||
/>
|
||||
) : null;
|
||||
const markAsReadOption: JSX.Element | null = (() => {
|
||||
if (level > NotificationLevel.None) {
|
||||
return (
|
||||
<IconizedContextMenuOption
|
||||
onClick={wrapHandler(() => {
|
||||
clearRoomNotification(room, cli);
|
||||
onFinished?.();
|
||||
}, onPostMarkAsReadClick)}
|
||||
label={_t("room|context_menu|mark_read")}
|
||||
iconClassName="mx_RoomGeneralContextMenu_iconMarkAsRead"
|
||||
/>
|
||||
);
|
||||
} else if (!roomTags.includes(DefaultTagID.Archived)) {
|
||||
return (
|
||||
<IconizedContextMenuOption
|
||||
onClick={wrapHandler(() => {
|
||||
setMarkedUnreadState(room, cli, true);
|
||||
onFinished?.();
|
||||
}, onPostMarkAsUnreadClick)}
|
||||
label={_t("room|context_menu|mark_unread")}
|
||||
iconClassName="mx_RoomGeneralContextMenu_iconMarkAsUnread"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const developerModeEnabled = useSettingValue<boolean>("developerMode");
|
||||
const developerToolsOption = developerModeEnabled ? (
|
||||
|
|
|
@ -60,7 +60,7 @@ const BaseTool: React.FC<XOR<IMinProps, IProps>> = ({
|
|||
let actionButton: ReactNode = null;
|
||||
if (message) {
|
||||
children = message;
|
||||
} else if (onAction) {
|
||||
} else if (onAction && actionLabel) {
|
||||
const onActionClick = (): void => {
|
||||
onAction().then((msg) => {
|
||||
if (typeof msg === "string") {
|
||||
|
|
|
@ -38,7 +38,7 @@ export const StateEventEditor: React.FC<IEditorProps> = ({ mxEvent, onBack }) =>
|
|||
);
|
||||
|
||||
const onSend = async ([eventType, stateKey]: string[], content: IContent): Promise<void> => {
|
||||
await cli.sendStateEvent(context.room.roomId, eventType, content, stateKey);
|
||||
await cli.sendStateEvent(context.room.roomId, eventType as any, content, stateKey);
|
||||
};
|
||||
|
||||
const defaultContent = mxEvent ? stringify(mxEvent.getContent()) : undefined;
|
||||
|
|
|
@ -430,7 +430,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
|
|||
onPrimaryButtonClick={this.onRecoveryKeyNext}
|
||||
hasCancel={true}
|
||||
cancelButton={_t("action|go_back")}
|
||||
cancelButtonClass="danger"
|
||||
cancelButtonClass="warning"
|
||||
onCancel={this.onCancel}
|
||||
focus={false}
|
||||
primaryDisabled={!this.state.recoveryKeyValid}
|
||||
|
|
|
@ -16,22 +16,23 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import hljs from "highlight.js";
|
||||
|
||||
interface IProps {
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
|
||||
interface Props {
|
||||
language?: string;
|
||||
children: string;
|
||||
}
|
||||
|
||||
export default class SyntaxHighlight extends React.PureComponent<IProps> {
|
||||
public render(): React.ReactNode {
|
||||
const { children: content, language } = this.props;
|
||||
const highlighted = language ? hljs.highlight(content, { language }) : hljs.highlightAuto(content);
|
||||
export default function SyntaxHighlight({ children, language }: Props): JSX.Element {
|
||||
const highlighted = useAsyncMemo(async () => {
|
||||
const { default: highlight } = await import("highlight.js");
|
||||
return language ? highlight.highlight(children, { language }) : highlight.highlightAuto(children);
|
||||
}, [language, children]);
|
||||
|
||||
return (
|
||||
<pre className={`mx_SyntaxHighlight hljs language-${highlighted.language}`}>
|
||||
<code dangerouslySetInnerHTML={{ __html: highlighted.value }} />
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<pre className={`mx_SyntaxHighlight hljs language-${highlighted?.language}`}>
|
||||
{highlighted ? <code dangerouslySetInnerHTML={{ __html: highlighted.value }} /> : children}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import { aboveLeftOf, useContextMenu, MenuProps } from "../../structures/Context
|
|||
import { OverflowMenuContext } from "../rooms/MessageComposerButtons";
|
||||
import LocationShareMenu from "./LocationShareMenu";
|
||||
|
||||
interface IProps {
|
||||
export interface IProps {
|
||||
roomId: string;
|
||||
sender: RoomMember;
|
||||
menuPosition?: MenuProps;
|
||||
|
|
|
@ -139,7 +139,7 @@ const onGeolocateError = (e: GeolocationPositionError): void => {
|
|||
});
|
||||
};
|
||||
|
||||
interface MapProps {
|
||||
export interface MapProps {
|
||||
id: string;
|
||||
interactive?: boolean;
|
||||
/**
|
||||
|
|
|
@ -18,7 +18,8 @@ import React, { ReactNode, useCallback, useEffect, useState } from "react";
|
|||
import * as maplibregl from "maplibre-gl";
|
||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { createMarker, parseGeoUri } from "../../../utils/location";
|
||||
import { parseGeoUri } from "../../../utils/location";
|
||||
import { createMarker } from "../../../utils/location/map";
|
||||
import Marker from "./Marker";
|
||||
|
||||
const useMapMarker = (
|
||||
|
@ -66,7 +67,7 @@ const useMapMarker = (
|
|||
};
|
||||
};
|
||||
|
||||
interface SmartMarkerProps {
|
||||
export interface SmartMarkerProps {
|
||||
map: maplibregl.Map;
|
||||
geoUri: string;
|
||||
id?: string;
|
||||
|
|
71
src/components/views/location/index.tsx
Normal file
71
src/components/views/location/index.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
Copyright 2024 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.
|
||||
*/
|
||||
|
||||
// Exports location components which touch maplibre-gs wrapped in React Suspense to enable code splitting
|
||||
|
||||
import React, { ComponentProps, lazy, Suspense } from "react";
|
||||
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
const MapComponent = lazy(() => import("./Map"));
|
||||
|
||||
export function Map(props: ComponentProps<typeof MapComponent>): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<MapComponent {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const LocationPickerComponent = lazy(() => import("./LocationPicker"));
|
||||
|
||||
export function LocationPicker(props: ComponentProps<typeof LocationPickerComponent>): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<LocationPickerComponent {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const SmartMarkerComponent = lazy(() => import("./SmartMarker"));
|
||||
|
||||
export function SmartMarker(props: ComponentProps<typeof SmartMarkerComponent>): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<SmartMarkerComponent {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const LocationButtonComponent = lazy(() => import("./LocationButton"));
|
||||
|
||||
export function LocationButton(props: ComponentProps<typeof LocationButtonComponent>): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<LocationButtonComponent {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const LocationViewDialogComponent = lazy(() => import("./LocationViewDialog"));
|
||||
|
||||
export function LocationViewDialog(props: ComponentProps<typeof LocationViewDialogComponent>): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<LocationViewDialogComponent {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
|
@ -38,12 +38,11 @@ import { isSelfLocation, LocationShareError } from "../../../utils/location";
|
|||
import { BeaconDisplayStatus, getBeaconDisplayStatus } from "../beacon/displayStatus";
|
||||
import BeaconStatus from "../beacon/BeaconStatus";
|
||||
import OwnBeaconStatus from "../beacon/OwnBeaconStatus";
|
||||
import Map from "../location/Map";
|
||||
import { Map, SmartMarker } from "../location";
|
||||
import { MapError } from "../location/MapError";
|
||||
import MapFallback from "../location/MapFallback";
|
||||
import SmartMarker from "../location/SmartMarker";
|
||||
import { GetRelationsForEvent } from "../rooms/EventTile";
|
||||
import BeaconViewDialog from "../beacon/BeaconViewDialog";
|
||||
import { BeaconViewDialog } from "../beacon";
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
|
||||
const useBeaconState = (
|
||||
|
|
|
@ -29,9 +29,7 @@ import {
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import TooltipTarget from "../elements/TooltipTarget";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
import LocationViewDialog from "../location/LocationViewDialog";
|
||||
import Map from "../location/Map";
|
||||
import SmartMarker from "../location/SmartMarker";
|
||||
import { SmartMarker, Map, LocationViewDialog } from "../location";
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
import { createReconnectedListener } from "../../../utils/connection";
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React, { createRef, SyntheticEvent, MouseEvent } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import highlight from "highlight.js";
|
||||
import { MsgType } from "matrix-js-sdk/src/matrix";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
|
||||
|
@ -238,7 +237,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
pre.append(document.createElement("span"));
|
||||
}
|
||||
|
||||
private highlightCode(code: HTMLElement): void {
|
||||
private async highlightCode(code: HTMLElement): Promise<void> {
|
||||
const { default: highlight } = await import("highlight.js");
|
||||
|
||||
if (code.textContent && code.textContent.length > MAX_HIGHLIGHT_LENGTH) {
|
||||
console.log(
|
||||
"Code block is bigger than highlight limit (" +
|
||||
|
|
|
@ -15,8 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { ChangeEvent, ContextType, createRef, SyntheticEvent } from "react";
|
||||
import { IContent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { RoomCanonicalAliasEventContent } from "matrix-js-sdk/src/types";
|
||||
|
||||
import EditableItemList from "../elements/EditableItemList";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -169,7 +170,7 @@ export default class AliasSettings extends React.Component<IProps, IState> {
|
|||
updatingCanonicalAlias: true,
|
||||
});
|
||||
|
||||
const eventContent: IContent = {
|
||||
const eventContent: RoomCanonicalAliasEventContent = {
|
||||
alt_aliases: this.state.altAliases,
|
||||
};
|
||||
|
||||
|
@ -197,7 +198,7 @@ export default class AliasSettings extends React.Component<IProps, IState> {
|
|||
updatingCanonicalAlias: true,
|
||||
});
|
||||
|
||||
const eventContent: IContent = {};
|
||||
const eventContent: RoomCanonicalAliasEventContent = {};
|
||||
|
||||
if (this.state.canonicalAlias) {
|
||||
eventContent["alias"] = this.state.canonicalAlias;
|
||||
|
|
|
@ -24,7 +24,7 @@ import { CollapsibleButton } from "./CollapsibleButton";
|
|||
import { MenuProps } from "../../structures/ContextMenu";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import LocationButton from "../location/LocationButton";
|
||||
import { LocationButton } from "../location";
|
||||
import Modal from "../../../Modal";
|
||||
import PollCreateDialog from "../elements/PollCreateDialog";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
|
|
@ -102,7 +102,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
if (notification.isIdle && !notification.knocked) return null;
|
||||
if (hideIfDot && notification.level < NotificationLevel.Notification) {
|
||||
// This would just be a dot and we've been told not to show dots, so don't show it
|
||||
if (!notification.hasUnreadCount) return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
const commonProps: React.ComponentProps<typeof StatelessNotificationBadge> = {
|
||||
|
|
|
@ -70,6 +70,16 @@ export const StatelessNotificationBadge = forwardRef<HTMLDivElement, XOR<Props,
|
|||
symbol = formatCount(count);
|
||||
}
|
||||
|
||||
// We show a dot if either:
|
||||
// * The props force us to, or
|
||||
// * It's just an activity-level notification or (in theory) lower and the room isn't knocked
|
||||
const badgeType =
|
||||
forceDot || (level <= NotificationLevel.Activity && !knocked)
|
||||
? "dot"
|
||||
: !symbol || symbol.length < 3
|
||||
? "badge_2char"
|
||||
: "badge_3char";
|
||||
|
||||
const classes = classNames({
|
||||
mx_NotificationBadge: true,
|
||||
mx_NotificationBadge_visible: isEmptyBadge || knocked ? true : hasUnreadCount,
|
||||
|
@ -77,10 +87,10 @@ export const StatelessNotificationBadge = forwardRef<HTMLDivElement, XOR<Props,
|
|||
mx_NotificationBadge_level_highlight: level >= NotificationLevel.Highlight,
|
||||
mx_NotificationBadge_knocked: knocked,
|
||||
|
||||
// At most one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char
|
||||
mx_NotificationBadge_dot: (isEmptyBadge && !knocked) || forceDot,
|
||||
mx_NotificationBadge_2char: !forceDot && symbol && symbol.length > 0 && symbol.length < 3,
|
||||
mx_NotificationBadge_3char: !forceDot && symbol && symbol.length > 2,
|
||||
// Exactly one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char
|
||||
mx_NotificationBadge_dot: badgeType === "dot",
|
||||
mx_NotificationBadge_2char: badgeType === "badge_2char",
|
||||
mx_NotificationBadge_3char: badgeType === "badge_3char",
|
||||
});
|
||||
|
||||
if (props.onClick) {
|
||||
|
|
|
@ -363,6 +363,12 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
onPostLeaveClick={(ev: ButtonEvent) =>
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", ev)
|
||||
}
|
||||
onPostMarkAsReadClick={(ev: ButtonEvent) =>
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkRead", ev)
|
||||
}
|
||||
onPostMarkAsUnreadClick={(ev: ButtonEvent) =>
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkUnread", ev)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixEvent, Room, RoomStateEvent, EventType } from "matrix-js-sdk/src/matrix";
|
||||
import { EventType, MatrixEvent, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Button, Text } from "@vector-im/compound-web";
|
||||
|
||||
|
@ -101,7 +101,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
|
||||
public onKickClick = (): void => {
|
||||
MatrixClientPeg.safeGet()
|
||||
.sendStateEvent(this.state.roomId, "m.room.third_party_invite", {}, this.state.stateKey)
|
||||
.sendStateEvent(this.state.roomId, EventType.RoomThirdPartyInvite, {}, this.state.stateKey)
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
|
||||
|
|
142
src/components/views/settings/PowerLevelSelector.tsx
Normal file
142
src/components/views/settings/PowerLevelSelector.tsx
Normal file
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2024 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, JSX, PropsWithChildren } from "react";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import { compare } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import PowerSelector from "../elements/PowerSelector";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsFieldset from "./SettingsFieldset";
|
||||
|
||||
/**
|
||||
* Display in a fieldset, the power level of the users and allow to change them.
|
||||
* The apply button is disabled until the power level of an user is changed.
|
||||
* If there is no user to display, the children is displayed instead.
|
||||
*/
|
||||
interface PowerLevelSelectorProps {
|
||||
/**
|
||||
* The power levels of the users
|
||||
* The key is the user id and the value is the power level
|
||||
*/
|
||||
userLevels: Record<string, number>;
|
||||
/**
|
||||
* Whether the user can change the power levels of other users
|
||||
*/
|
||||
canChangeLevels: boolean;
|
||||
/**
|
||||
* The current user power level
|
||||
*/
|
||||
currentUserLevel: number;
|
||||
/**
|
||||
* The callback when the apply button is clicked
|
||||
* @param value - new power level for the user
|
||||
* @param userId - the user id
|
||||
*/
|
||||
onClick: (value: number, userId: string) => void;
|
||||
/**
|
||||
* Filter the users to display
|
||||
* @param user
|
||||
*/
|
||||
filter: (user: string) => boolean;
|
||||
/**
|
||||
* The title of the fieldset
|
||||
*/
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function PowerLevelSelector({
|
||||
userLevels,
|
||||
canChangeLevels,
|
||||
currentUserLevel,
|
||||
onClick,
|
||||
filter,
|
||||
title,
|
||||
children,
|
||||
}: PropsWithChildren<PowerLevelSelectorProps>): JSX.Element | null {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const [currentPowerLevel, setCurrentPowerLevel] = useState<{ value: number; userId: string } | null>(null);
|
||||
|
||||
// If the power level has changed, we need to enable the apply button
|
||||
const powerLevelChanged = Boolean(
|
||||
currentPowerLevel && currentPowerLevel.value !== userLevels[currentPowerLevel?.userId],
|
||||
);
|
||||
|
||||
// We sort the users by power level, then we filter them
|
||||
const users = Object.keys(userLevels)
|
||||
.sort((userA, userB) => sortUser(userA, userB, userLevels))
|
||||
.filter(filter);
|
||||
|
||||
// No user to display, we return the children into fragment to convert it to JSX.Element type
|
||||
if (!users.length) return <>{children}</>;
|
||||
|
||||
return (
|
||||
<SettingsFieldset legend={title}>
|
||||
{users.map((userId) => {
|
||||
// We only want to display users with a valid power level aka an integer
|
||||
if (!Number.isInteger(userLevels[userId])) return;
|
||||
|
||||
const isMe = userId === matrixClient.getUserId();
|
||||
// If I can change levels, I can change the level of anyone with a lower level than mine
|
||||
const canChange = canChangeLevels && (userLevels[userId] < currentUserLevel || isMe);
|
||||
|
||||
// When the new power level is selected, the fields are rerendered and we need to keep the current value
|
||||
const userLevel = currentPowerLevel?.userId === userId ? currentPowerLevel?.value : userLevels[userId];
|
||||
|
||||
return (
|
||||
<PowerSelector
|
||||
value={userLevel}
|
||||
disabled={!canChange}
|
||||
label={userId}
|
||||
key={userId}
|
||||
onChange={(value) => setCurrentPowerLevel({ value, userId })}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
kind="primary"
|
||||
// mx_Dialog_nonDialogButton is necessary to avoid the Dialog CSS to override the button style
|
||||
className="mx_Dialog_nonDialogButton mx_PowerLevelSelector_Button"
|
||||
onClick={() => {
|
||||
if (currentPowerLevel !== null) {
|
||||
onClick(currentPowerLevel.value, currentPowerLevel.userId);
|
||||
setCurrentPowerLevel(null);
|
||||
}
|
||||
}}
|
||||
disabled={!powerLevelChanged}
|
||||
aria-label={_t("action|apply")}
|
||||
>
|
||||
{_t("action|apply")}
|
||||
</Button>
|
||||
</SettingsFieldset>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the users by power level, then by name
|
||||
* @param userA
|
||||
* @param userB
|
||||
* @param userLevels
|
||||
*/
|
||||
function sortUser(userA: string, userB: string, userLevels: PowerLevelSelectorProps["userLevels"]): number {
|
||||
const powerLevelDiff = userLevels[userA] - userLevels[userB];
|
||||
return powerLevelDiff !== 0 ? powerLevelDiff : compare(userA.toLocaleLowerCase(), userB.toLocaleLowerCase());
|
||||
}
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
||||
import { IAuthData, ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t, UserFriendlyError } from "../../../../languageHandler";
|
||||
|
@ -216,7 +216,7 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
|
|||
const address = this.state.verifyMsisdn;
|
||||
this.state.addTask
|
||||
?.haveMsisdnToken(token)
|
||||
.then(([finished] = []) => {
|
||||
.then(([finished]: [success?: boolean, result?: IAuthData | Error | null] = []) => {
|
||||
let newPhoneNumber = this.state.newPhoneNumber;
|
||||
if (finished !== false) {
|
||||
const msisdns = [...this.props.msisdns, { address, medium: ThreepidMedium.Phone }];
|
||||
|
|
|
@ -19,7 +19,7 @@ import { EventType, RoomMember, RoomState, RoomStateEvent, Room, IContent } from
|
|||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { throttle, get } from "lodash";
|
||||
import { compare } from "matrix-js-sdk/src/utils";
|
||||
import { RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { _t, _td, TranslationKey } from "../../../../../languageHandler";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
|
@ -35,6 +35,7 @@ import { AddPrivilegedUsers } from "../../AddPrivilegedUsers";
|
|||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import { PowerLevelSelector } from "../../PowerLevelSelector";
|
||||
|
||||
interface IEventShowOpts {
|
||||
isState?: boolean;
|
||||
|
@ -179,7 +180,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
const client = this.context;
|
||||
const room = this.props.room;
|
||||
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
let plContent = plEvent?.getContent() ?? {};
|
||||
let plContent = plEvent?.getContent<RoomPowerLevelsEventContent>() ?? {};
|
||||
|
||||
// Clone the power levels just in case
|
||||
plContent = Object.assign({}, plContent);
|
||||
|
@ -193,7 +194,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
} else {
|
||||
const keyPath = powerLevelKey.split(".");
|
||||
let parentObj: IContent = {};
|
||||
let currentObj = plContent;
|
||||
let currentObj: IContent = plContent;
|
||||
for (const key of keyPath) {
|
||||
if (!currentObj[key]) {
|
||||
currentObj[key] = {};
|
||||
|
@ -223,7 +224,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
const client = this.context;
|
||||
const room = this.props.room;
|
||||
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
let plContent = plEvent?.getContent() ?? {};
|
||||
let plContent = plEvent?.getContent<RoomPowerLevelsEventContent>() ?? {};
|
||||
|
||||
// Clone the power levels just in case
|
||||
plContent = Object.assign({}, plContent);
|
||||
|
@ -241,9 +242,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
title: _t("room_settings|permissions|error_changing_pl_title"),
|
||||
description: _t("room_settings|permissions|error_changing_pl_description"),
|
||||
});
|
||||
|
||||
// Rethrow so that the PowerSelector can roll back
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -353,65 +351,29 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
let privilegedUsersSection = <div>{_t("room_settings|permissions|no_privileged_users")}</div>;
|
||||
let mutedUsersSection;
|
||||
if (Object.keys(userLevels).length) {
|
||||
const privilegedUsers: JSX.Element[] = [];
|
||||
const mutedUsers: JSX.Element[] = [];
|
||||
privilegedUsersSection = (
|
||||
<PowerLevelSelector
|
||||
title={_t("room_settings|permissions|privileged_users_section")}
|
||||
userLevels={userLevels}
|
||||
canChangeLevels={canChangeLevels}
|
||||
currentUserLevel={currentUserLevel}
|
||||
onClick={this.onUserPowerLevelChanged}
|
||||
filter={(user) => userLevels[user] > defaultUserLevel}
|
||||
>
|
||||
<div>{_t("room_settings|permissions|no_privileged_users")}</div>
|
||||
</PowerLevelSelector>
|
||||
);
|
||||
|
||||
Object.keys(userLevels).forEach((user) => {
|
||||
if (!Number.isInteger(userLevels[user])) return;
|
||||
const isMe = user === client.getUserId();
|
||||
const canChange = canChangeLevels && (userLevels[user] < currentUserLevel || isMe);
|
||||
if (userLevels[user] > defaultUserLevel) {
|
||||
// privileged
|
||||
privilegedUsers.push(
|
||||
<PowerSelector
|
||||
value={userLevels[user]}
|
||||
disabled={!canChange}
|
||||
label={user}
|
||||
key={user}
|
||||
powerLevelKey={user} // Will be sent as the second parameter to `onChange`
|
||||
onChange={this.onUserPowerLevelChanged}
|
||||
/>,
|
||||
);
|
||||
} else if (userLevels[user] < defaultUserLevel) {
|
||||
// muted
|
||||
mutedUsers.push(
|
||||
<PowerSelector
|
||||
value={userLevels[user]}
|
||||
disabled={!canChange}
|
||||
label={user}
|
||||
key={user}
|
||||
powerLevelKey={user} // Will be sent as the second parameter to `onChange`
|
||||
onChange={this.onUserPowerLevelChanged}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
|
||||
const comparator = (a: JSX.Element, b: JSX.Element): number => {
|
||||
const aKey = a.key as string;
|
||||
const bKey = b.key as string;
|
||||
const plDiff = userLevels[bKey] - userLevels[aKey];
|
||||
return plDiff !== 0 ? plDiff : compare(aKey.toLocaleLowerCase(), bKey.toLocaleLowerCase());
|
||||
};
|
||||
|
||||
privilegedUsers.sort(comparator);
|
||||
mutedUsers.sort(comparator);
|
||||
|
||||
if (privilegedUsers.length) {
|
||||
privilegedUsersSection = (
|
||||
<SettingsFieldset legend={_t("room_settings|permissions|privileged_users_section")}>
|
||||
{privilegedUsers}
|
||||
</SettingsFieldset>
|
||||
);
|
||||
}
|
||||
if (mutedUsers.length) {
|
||||
mutedUsersSection = (
|
||||
<SettingsFieldset legend={_t("room_settings|permissions|muted_users_section")}>
|
||||
{mutedUsers}
|
||||
</SettingsFieldset>
|
||||
);
|
||||
}
|
||||
mutedUsersSection = (
|
||||
<PowerLevelSelector
|
||||
title={_t("room_settings|permissions|muted_users_section")}
|
||||
userLevels={userLevels}
|
||||
canChangeLevels={canChangeLevels}
|
||||
currentUserLevel={currentUserLevel}
|
||||
onClick={this.onUserPowerLevelChanged}
|
||||
filter={(user) => userLevels[user] < defaultUserLevel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const banned = room.getMembersWithMembership(KnownMembership.Ban);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue