Merge branch 'develop' into andybalaam/stas-demydiuk-membership-type3
This commit is contained in:
@@ -16,12 +16,7 @@ limitations under the License.
|
||||
|
||||
import { JSXElementConstructor } from "react";
|
||||
|
||||
export type { NonEmptyArray } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
// Based on https://stackoverflow.com/a/53229857/3532235
|
||||
export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
export type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
||||
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
export type { NonEmptyArray, XOR, Writeable } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
|
||||
|
||||
|
||||
6
src/@types/global.d.ts
vendored
6
src/@types/global.d.ts
vendored
@@ -151,16 +151,10 @@ declare global {
|
||||
|
||||
interface HTMLAudioElement {
|
||||
type?: string;
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
sinkId: string;
|
||||
setSinkId(outputId: string): void;
|
||||
}
|
||||
|
||||
interface HTMLVideoElement {
|
||||
type?: string;
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
sinkId: string;
|
||||
setSinkId(outputId: string): void;
|
||||
}
|
||||
|
||||
// Add Chrome-specific `instant` ScrollBehaviour
|
||||
|
||||
30
src/@types/matrix-js-sdk.d.ts
vendored
30
src/@types/matrix-js-sdk.d.ts
vendored
@@ -14,14 +14,40 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { BLURHASH_FIELD } from "../utils/image-media";
|
||||
import type { IWidget } from "matrix-widget-api";
|
||||
import type { BLURHASH_FIELD } from "../utils/image-media";
|
||||
import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types";
|
||||
import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types";
|
||||
import type { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType } from "../voice-broadcast/types";
|
||||
|
||||
// Matrix JS SDK extensions
|
||||
declare module "matrix-js-sdk" {
|
||||
declare module "matrix-js-sdk/src/types" {
|
||||
export interface FileInfo {
|
||||
/**
|
||||
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/2448
|
||||
*/
|
||||
[BLURHASH_FIELD]?: string;
|
||||
}
|
||||
|
||||
export interface StateEvents {
|
||||
// Jitsi-backed video room state events
|
||||
[JitsiCallMemberEventType]: JitsiCallMemberContent;
|
||||
|
||||
// Unstable widgets state events
|
||||
"im.vector.modular.widgets": IWidget | {};
|
||||
[WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent;
|
||||
|
||||
// Unstable voice broadcast state events
|
||||
[VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent;
|
||||
|
||||
// Element custom state events
|
||||
"im.vector.web.settings": Record<string, any>;
|
||||
"org.matrix.room.preview_urls": { disable: boolean };
|
||||
|
||||
// XXX unspecced usages of `m.room.*` events
|
||||
"m.room.plumbing": {
|
||||
status: string;
|
||||
};
|
||||
"m.room.bot.options": unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getUnsentMessages } from "./components/structures/RoomStatusBar";
|
||||
import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread";
|
||||
import { EffectiveMembership, getEffectiveMembership, isKnockDenied } from "./utils/membership";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { getMarkedUnreadState } from "./utils/notifications";
|
||||
|
||||
export enum RoomNotifState {
|
||||
AllMessagesLoud = "all_messages_loud",
|
||||
@@ -279,7 +280,8 @@ export function determineUnreadState(
|
||||
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight };
|
||||
}
|
||||
|
||||
if (greyNotifs > 0) {
|
||||
const markedUnreadState = getMarkedUnreadState(room);
|
||||
if (greyNotifs > 0 || markedUnreadState) {
|
||||
return { symbol: null, count: trueCount, level: NotificationLevel.Notification };
|
||||
}
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ Response:
|
||||
|
||||
*/
|
||||
|
||||
import { IContent, MatrixEvent, IEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { IContent, MatrixEvent, IEvent, StateEvents } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@@ -725,7 +725,7 @@ async function getOpenIdToken(event: MessageEvent<any>): Promise<void> {
|
||||
|
||||
async function sendEvent(
|
||||
event: MessageEvent<{
|
||||
type: string;
|
||||
type: keyof StateEvents;
|
||||
state_key?: string;
|
||||
content?: IContent;
|
||||
}>,
|
||||
|
||||
@@ -18,9 +18,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { User, IContent, Direction, ContentHelpers, MRoomTopicEventContent } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { ContentHelpers, Direction, EventType, IContent, MRoomTopicEventContent, User } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { KnownMembership, RoomMemberEventContent } from "matrix-js-sdk/src/types";
|
||||
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import { _t, _td, UserFriendlyError } from "./languageHandler";
|
||||
@@ -240,12 +240,12 @@ export const Commands = [
|
||||
isEnabled: (cli) => !isCurrentLocalRoom(cli),
|
||||
runFn: function (cli, roomId, threadId, args) {
|
||||
if (args) {
|
||||
const ev = cli.getRoom(roomId)?.currentState.getStateEvents("m.room.member", cli.getSafeUserId());
|
||||
const content = {
|
||||
const ev = cli.getRoom(roomId)?.currentState.getStateEvents(EventType.RoomMember, cli.getSafeUserId());
|
||||
const content: RoomMemberEventContent = {
|
||||
...(ev ? ev.getContent() : { membership: KnownMembership.Join }),
|
||||
displayname: args,
|
||||
};
|
||||
return success(cli.sendStateEvent(roomId, "m.room.member", content, cli.getSafeUserId()));
|
||||
return success(cli.sendStateEvent(roomId, EventType.RoomMember, content, cli.getSafeUserId()));
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
@@ -266,7 +266,7 @@ export const Commands = [
|
||||
return success(
|
||||
promise.then((url) => {
|
||||
if (!url) return;
|
||||
return cli.sendStateEvent(roomId, "m.room.avatar", { url }, "");
|
||||
return cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, "");
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -290,12 +290,12 @@ export const Commands = [
|
||||
return success(
|
||||
promise.then((url) => {
|
||||
if (!url) return;
|
||||
const ev = room?.currentState.getStateEvents("m.room.member", userId);
|
||||
const content = {
|
||||
const ev = room?.currentState.getStateEvents(EventType.RoomMember, userId);
|
||||
const content: RoomMemberEventContent = {
|
||||
...(ev ? ev.getContent() : { membership: KnownMembership.Join }),
|
||||
avatar_url: url,
|
||||
};
|
||||
return cli.sendStateEvent(roomId, "m.room.member", content, userId);
|
||||
return cli.sendStateEvent(roomId, EventType.RoomMember, content, userId);
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -17,3 +17,12 @@ limitations under the License.
|
||||
// Event type for room account data and room creation content used to mark rooms as virtual rooms
|
||||
// (and store the ID of their native room)
|
||||
export const VIRTUAL_ROOM_EVENT_TYPE = "im.vector.is_virtual_room";
|
||||
|
||||
export const JitsiCallMemberEventType = "io.element.video.member";
|
||||
|
||||
export interface JitsiCallMemberContent {
|
||||
// Connected device IDs
|
||||
devices: string[];
|
||||
// Time at which this state event should be considered stale
|
||||
expires_ts: number;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
|
||||
import classNames from "classnames";
|
||||
import { sortBy, uniqBy } from "lodash";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { SpaceChildEventContent } from "matrix-js-sdk/src/types";
|
||||
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
@@ -727,7 +728,7 @@ const ManageButtons: React.FC<IManageButtonsProps> = ({ hierarchy, selected, set
|
||||
const existingContent = hierarchy.getRelation(parentId, childId)?.content;
|
||||
if (!existingContent || existingContent.suggested === suggested) continue;
|
||||
|
||||
const content = {
|
||||
const content: SpaceChildEventContent = {
|
||||
...existingContent,
|
||||
suggested: !selectionAllSuggested,
|
||||
};
|
||||
|
||||
@@ -1046,7 +1046,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
if (
|
||||
!(await client.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) ||
|
||||
!(await client.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) &&
|
||||
!(await client.isVersionSupported("v1.4"))
|
||||
) {
|
||||
logger.warn(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1892,6 +1892,7 @@
|
||||
"forget": "Forget Room",
|
||||
"low_priority": "Low Priority",
|
||||
"mark_read": "Mark as read",
|
||||
"mark_unread": "Mark as unread",
|
||||
"mentions_only": "Mentions only",
|
||||
"notifications_default": "Match default setting",
|
||||
"notifications_mute": "Mute room",
|
||||
|
||||
@@ -16,12 +16,14 @@ limitations under the License.
|
||||
|
||||
// Inspiration largely taken from Mjolnir itself
|
||||
|
||||
import { EventType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { ListRule, RECOMMENDATION_BAN, recommendationToStable } from "./ListRule";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
|
||||
export const RULE_USER = "m.policy.rule.user";
|
||||
export const RULE_ROOM = "m.policy.rule.room";
|
||||
export const RULE_SERVER = "m.policy.rule.server";
|
||||
export const RULE_USER = EventType.PolicyRuleUser;
|
||||
export const RULE_ROOM = EventType.PolicyRuleRoom;
|
||||
export const RULE_SERVER = EventType.PolicyRuleServer;
|
||||
|
||||
// m.room.* events are legacy from when MSC2313 changed to m.policy.* last minute.
|
||||
export const USER_RULE_TYPES = [RULE_USER, "m.room.rule.user", "org.matrix.mjolnir.rule.user"];
|
||||
@@ -29,7 +31,9 @@ export const ROOM_RULE_TYPES = [RULE_ROOM, "m.room.rule.room", "org.matrix.mjoln
|
||||
export const SERVER_RULE_TYPES = [RULE_SERVER, "m.room.rule.server", "org.matrix.mjolnir.rule.server"];
|
||||
export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES];
|
||||
|
||||
export function ruleTypeToStable(rule: string): string | null {
|
||||
export function ruleTypeToStable(
|
||||
rule: string,
|
||||
): EventType.PolicyRuleUser | EventType.PolicyRuleRoom | EventType.PolicyRuleServer | null {
|
||||
if (USER_RULE_TYPES.includes(rule)) {
|
||||
return RULE_USER;
|
||||
}
|
||||
@@ -72,7 +76,7 @@ export class BanList {
|
||||
{
|
||||
entity: entity,
|
||||
reason: reason,
|
||||
recommendation: recommendationToStable(RECOMMENDATION_BAN, true),
|
||||
recommendation: recommendationToStable(RECOMMENDATION_BAN, true)!,
|
||||
},
|
||||
"rule:" + entity,
|
||||
);
|
||||
|
||||
@@ -14,14 +14,24 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// We are using experimental APIs here, so we need to disable the linter
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PolicyRecommendation } from "matrix-js-sdk/src/models/invites-ignorer";
|
||||
|
||||
import { MatrixGlob } from "../utils/MatrixGlob";
|
||||
|
||||
// Inspiration largely taken from Mjolnir itself
|
||||
|
||||
export const RECOMMENDATION_BAN = "m.ban";
|
||||
export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"];
|
||||
export const RECOMMENDATION_BAN = PolicyRecommendation.Ban;
|
||||
export const RECOMMENDATION_BAN_TYPES: PolicyRecommendation[] = [
|
||||
RECOMMENDATION_BAN,
|
||||
"org.matrix.mjolnir.ban" as PolicyRecommendation,
|
||||
];
|
||||
|
||||
export function recommendationToStable(recommendation: string, unstable = true): string | null {
|
||||
export function recommendationToStable(
|
||||
recommendation: PolicyRecommendation,
|
||||
unstable = true,
|
||||
): PolicyRecommendation | null {
|
||||
if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) {
|
||||
return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN;
|
||||
}
|
||||
@@ -35,7 +45,7 @@ export class ListRule {
|
||||
private readonly _reason: string;
|
||||
private readonly _kind: string;
|
||||
|
||||
public constructor(entity: string, action: string, reason: string, kind: string) {
|
||||
public constructor(entity: string, action: PolicyRecommendation, reason: string, kind: string) {
|
||||
this._glob = new MatrixGlob(entity);
|
||||
this._entity = entity;
|
||||
this._action = recommendationToStable(action, false);
|
||||
|
||||
@@ -58,6 +58,7 @@ import { UPDATE_EVENT } from "../stores/AsyncStore";
|
||||
import { getJoinedNonFunctionalMembers } from "../utils/room/getJoinedNonFunctionalMembers";
|
||||
import { isVideoRoom } from "../utils/video-rooms";
|
||||
import { FontWatcher } from "../settings/watchers/FontWatcher";
|
||||
import { JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types";
|
||||
|
||||
const TIMEOUT_MS = 16000;
|
||||
|
||||
@@ -322,18 +323,13 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
private beforeUnload = (): void => this.setDisconnected();
|
||||
}
|
||||
|
||||
export interface JitsiCallMemberContent {
|
||||
// Connected device IDs
|
||||
devices: string[];
|
||||
// Time at which this state event should be considered stale
|
||||
expires_ts: number;
|
||||
}
|
||||
export type { JitsiCallMemberContent };
|
||||
|
||||
/**
|
||||
* A group call using Jitsi as a backend.
|
||||
*/
|
||||
export class JitsiCall extends Call {
|
||||
public static readonly MEMBER_EVENT_TYPE = "io.element.video.member";
|
||||
public static readonly MEMBER_EVENT_TYPE = JitsiCallMemberEventType;
|
||||
public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
private resendDevicesTimer: number | null = null;
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient, MatrixEvent, RoomState, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient, MatrixEvent, RoomState, RoomStateEvent, StateEvents } from "matrix-js-sdk/src/matrix";
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
|
||||
@@ -24,6 +24,9 @@ import { SettingLevel } from "../SettingLevel";
|
||||
import { WatchManager } from "../WatchManager";
|
||||
|
||||
const DEFAULT_SETTINGS_EVENT_TYPE = "im.vector.web.settings";
|
||||
const PREVIEW_URLS_EVENT_TYPE = "org.matrix.room.preview_urls";
|
||||
|
||||
type RoomSettingsEventType = typeof DEFAULT_SETTINGS_EVENT_TYPE | typeof PREVIEW_URLS_EVENT_TYPE;
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "room" level.
|
||||
@@ -88,7 +91,12 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl
|
||||
}
|
||||
|
||||
// helper function to send state event then await it being echoed back
|
||||
private async sendStateEvent(roomId: string, eventType: string, field: string, value: any): Promise<void> {
|
||||
private async sendStateEvent<K extends RoomSettingsEventType, F extends keyof StateEvents[K]>(
|
||||
roomId: string,
|
||||
eventType: K,
|
||||
field: F,
|
||||
value: StateEvents[K][F],
|
||||
): Promise<void> {
|
||||
const content = this.getSettings(roomId, eventType) || {};
|
||||
content[field] = value;
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJoinPayload";
|
||||
import { SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload";
|
||||
import { ModuleRunner } from "../modules/ModuleRunner";
|
||||
import { setMarkedUnreadState } from "../utils/notifications";
|
||||
|
||||
const NUM_JOIN_RETRY = 5;
|
||||
|
||||
@@ -498,6 +499,8 @@ export class RoomViewStore extends EventEmitter {
|
||||
if (room) {
|
||||
pauseNonLiveBroadcastFromOtherRoom(room, this.stores.voiceBroadcastPlaybacksStore);
|
||||
this.doMaybeSetCurrentVoiceBroadcastPlayback(room);
|
||||
|
||||
await setMarkedUnreadState(room, MatrixClientPeg.safeGet(), false);
|
||||
}
|
||||
} else if (payload.room_alias) {
|
||||
// Try the room alias to room ID navigation cache first to avoid
|
||||
|
||||
@@ -24,6 +24,7 @@ import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||
import * as RoomNotifs from "../../RoomNotifs";
|
||||
import { NotificationState } from "./NotificationState";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { MARKED_UNREAD_TYPE_STABLE, MARKED_UNREAD_TYPE_UNSTABLE } from "../../utils/notifications";
|
||||
|
||||
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
||||
public constructor(
|
||||
@@ -37,6 +38,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||
this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
||||
this.room.on(RoomEvent.Timeline, this.handleRoomEventUpdate);
|
||||
this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
||||
this.room.on(RoomEvent.AccountData, this.handleRoomAccountDataUpdate);
|
||||
|
||||
this.room.on(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate); // for server-sent counts
|
||||
cli.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
@@ -52,6 +54,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||
this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
||||
this.room.removeListener(RoomEvent.Timeline, this.handleRoomEventUpdate);
|
||||
this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
||||
this.room.removeListener(RoomEvent.AccountData, this.handleRoomAccountDataUpdate);
|
||||
cli.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
cli.removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||
}
|
||||
@@ -91,6 +94,12 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||
}
|
||||
};
|
||||
|
||||
private handleRoomAccountDataUpdate = (ev: MatrixEvent): void => {
|
||||
if ([MARKED_UNREAD_TYPE_STABLE, MARKED_UNREAD_TYPE_UNSTABLE].includes(ev.getType())) {
|
||||
this.updateNotificationState();
|
||||
}
|
||||
};
|
||||
|
||||
private updateNotificationState(): void {
|
||||
const snapshot = this.snapshot();
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
Room,
|
||||
Direction,
|
||||
THREAD_RELATION_TYPE,
|
||||
StateEvents,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
@@ -241,7 +242,19 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||
return allAllowed;
|
||||
}
|
||||
|
||||
public async sendEvent<K extends keyof StateEvents>(
|
||||
eventType: K,
|
||||
content: StateEvents[K],
|
||||
stateKey?: string,
|
||||
targetRoomId?: string,
|
||||
): Promise<ISendEventDetails>;
|
||||
public async sendEvent(
|
||||
eventType: Exclude<EventType, keyof StateEvents>,
|
||||
content: IContent,
|
||||
stateKey: null,
|
||||
targetRoomId?: string,
|
||||
): Promise<ISendEventDetails>;
|
||||
public async sendEvent<K extends keyof StateEvents>(
|
||||
eventType: string,
|
||||
content: IContent,
|
||||
stateKey?: string | null,
|
||||
@@ -255,7 +268,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||
let r: { event_id: string } | null;
|
||||
if (stateKey !== null) {
|
||||
// state event
|
||||
r = await client.sendStateEvent(roomId, eventType, content, stateKey);
|
||||
r = await client.sendStateEvent(roomId, eventType as K, content as StateEvents[K], stateKey);
|
||||
} else if (eventType === EventType.RoomRedaction) {
|
||||
// special case: extract the `redacts` property and call redact
|
||||
r = await client.redactEvent(roomId, content["redacts"]);
|
||||
|
||||
@@ -28,55 +28,10 @@ import { ReadyWatchingStore } from "../ReadyWatchingStore";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
import { UPDATE_EVENT } from "../AsyncStore";
|
||||
import { Container, IStoredLayout, ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE, IWidgetLayouts } from "./types";
|
||||
|
||||
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
|
||||
|
||||
export enum Container {
|
||||
// "Top" is the app drawer, and currently the only sensible value.
|
||||
Top = "top",
|
||||
|
||||
// "Right" is the right panel, and the default for widgets. Setting
|
||||
// this as a container on a widget is essentially like saying "no
|
||||
// changes needed", though this may change in the future.
|
||||
Right = "right",
|
||||
|
||||
Center = "center",
|
||||
}
|
||||
|
||||
export interface IStoredLayout {
|
||||
// Where to store the widget. Required.
|
||||
container: Container;
|
||||
|
||||
// The index (order) to position the widgets in. Only applies for
|
||||
// ordered containers (like the top container). Smaller numbers first,
|
||||
// and conflicts resolved by comparing widget IDs.
|
||||
index?: number;
|
||||
|
||||
// Percentage (integer) for relative width of the container to consume.
|
||||
// Clamped to 0-100 and may have minimums imposed upon it. Only applies
|
||||
// to containers which support inner resizing (currently only the top
|
||||
// container).
|
||||
width?: number;
|
||||
|
||||
// Percentage (integer) for relative height of the container. Note that
|
||||
// this only applies to the top container currently, and that container
|
||||
// will take the highest value among widgets in the container. Clamped
|
||||
// to 0-100 and may have minimums imposed on it.
|
||||
height?: number | null;
|
||||
|
||||
// TODO: [Deferred] Maximizing (fullscreen) widgets by default.
|
||||
}
|
||||
|
||||
interface IWidgetLayouts {
|
||||
[widgetId: string]: IStoredLayout;
|
||||
}
|
||||
|
||||
interface ILayoutStateEvent {
|
||||
// TODO: [Deferred] Forced layout (fixed with no changes)
|
||||
|
||||
// The widget layouts.
|
||||
widgets: IWidgetLayouts;
|
||||
}
|
||||
export type { IStoredLayout, ILayoutStateEvent };
|
||||
export { Container, WIDGET_LAYOUT_EVENT_TYPE };
|
||||
|
||||
interface ILayoutSettings extends ILayoutStateEvent {
|
||||
overrides?: string; // event ID for layout state event, if present
|
||||
|
||||
64
src/stores/widgets/types.ts
Normal file
64
src/stores/widgets/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
export interface IStoredLayout {
|
||||
// Where to store the widget. Required.
|
||||
container: Container;
|
||||
|
||||
// The index (order) to position the widgets in. Only applies for
|
||||
// ordered containers (like the top container). Smaller numbers first,
|
||||
// and conflicts resolved by comparing widget IDs.
|
||||
index?: number;
|
||||
|
||||
// Percentage (integer) for relative width of the container to consume.
|
||||
// Clamped to 0-100 and may have minimums imposed upon it. Only applies
|
||||
// to containers which support inner resizing (currently only the top
|
||||
// container).
|
||||
width?: number;
|
||||
|
||||
// Percentage (integer) for relative height of the container. Note that
|
||||
// this only applies to the top container currently, and that container
|
||||
// will take the highest value among widgets in the container. Clamped
|
||||
// to 0-100 and may have minimums imposed on it.
|
||||
height?: number | null;
|
||||
|
||||
// TODO: [Deferred] Maximizing (fullscreen) widgets by default.
|
||||
}
|
||||
|
||||
export interface IWidgetLayouts {
|
||||
[widgetId: string]: IStoredLayout;
|
||||
}
|
||||
|
||||
export interface ILayoutStateEvent {
|
||||
// TODO: [Deferred] Forced layout (fixed with no changes)
|
||||
|
||||
// The widget layouts.
|
||||
widgets: IWidgetLayouts;
|
||||
}
|
||||
|
||||
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
|
||||
|
||||
export enum Container {
|
||||
// "Top" is the app drawer, and currently the only sensible value.
|
||||
Top = "top",
|
||||
|
||||
// "Right" is the right panel, and the default for widgets. Setting
|
||||
// this as a container on a widget is essentially like saying "no
|
||||
// changes needed", though this may change in the future.
|
||||
Right = "right",
|
||||
|
||||
Center = "center",
|
||||
}
|
||||
@@ -135,7 +135,7 @@ export async function upgradeRoom(
|
||||
EventType.SpaceChild,
|
||||
{
|
||||
...(currentEv?.getContent() || {}), // copy existing attributes like suggested
|
||||
via: [cli.getDomain()],
|
||||
via: [cli.getDomain()!],
|
||||
},
|
||||
newRoomId,
|
||||
);
|
||||
|
||||
@@ -27,13 +27,13 @@ export function textToHtmlRainbow(str: string): string {
|
||||
const [a, b] = generateAB(i * frequency, 1);
|
||||
const [red, green, blue] = labToRGB(75, a, b);
|
||||
return (
|
||||
'<font color="#' +
|
||||
'<span data-mx-color="#' +
|
||||
red.toString(16).padStart(2, "0") +
|
||||
green.toString(16).padStart(2, "0") +
|
||||
blue.toString(16).padStart(2, "0") +
|
||||
'">' +
|
||||
c +
|
||||
"</font>"
|
||||
"</span>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
|
||||
@@ -18,6 +18,6 @@ export * from "./findMapStyleUrl";
|
||||
export * from "./isSelfLocation";
|
||||
export * from "./locationEventGeoUri";
|
||||
export * from "./LocationShareErrors";
|
||||
export * from "./map";
|
||||
export * from "./links";
|
||||
export * from "./parseGeoUri";
|
||||
export * from "./positionFailureMessage";
|
||||
|
||||
47
src/utils/location/links.ts
Normal file
47
src/utils/location/links.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2022 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 { MatrixEvent, M_LOCATION } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { parseGeoUri } from "./parseGeoUri";
|
||||
|
||||
export const makeMapSiteLink = (coords: GeolocationCoordinates): string => {
|
||||
return (
|
||||
"https://www.openstreetmap.org/" +
|
||||
`?mlat=${coords.latitude}` +
|
||||
`&mlon=${coords.longitude}` +
|
||||
`#map=16/${coords.latitude}/${coords.longitude}`
|
||||
);
|
||||
};
|
||||
|
||||
export const createMapSiteLinkFromEvent = (event: MatrixEvent): string | null => {
|
||||
const content = event.getContent();
|
||||
const mLocation = content[M_LOCATION.name];
|
||||
if (mLocation !== undefined) {
|
||||
const uri = mLocation["uri"];
|
||||
if (uri !== undefined) {
|
||||
const geoCoords = parseGeoUri(uri);
|
||||
return geoCoords ? makeMapSiteLink(geoCoords) : null;
|
||||
}
|
||||
} else {
|
||||
const geoUri = content["geo_uri"];
|
||||
if (geoUri) {
|
||||
const geoCoords = parseGeoUri(geoUri);
|
||||
return geoCoords ? makeMapSiteLink(geoCoords) : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -15,11 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import * as maplibregl from "maplibre-gl";
|
||||
import { MatrixClient, MatrixEvent, M_LOCATION } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from "../../languageHandler";
|
||||
import { parseGeoUri } from "./parseGeoUri";
|
||||
import { findMapStyleUrl } from "./findMapStyleUrl";
|
||||
import { LocationShareError } from "./LocationShareErrors";
|
||||
|
||||
@@ -75,31 +74,3 @@ export const createMarker = (coords: GeolocationCoordinates, element: HTMLElemen
|
||||
}).setLngLat({ lon: coords.longitude, lat: coords.latitude });
|
||||
return marker;
|
||||
};
|
||||
|
||||
export const makeMapSiteLink = (coords: GeolocationCoordinates): string => {
|
||||
return (
|
||||
"https://www.openstreetmap.org/" +
|
||||
`?mlat=${coords.latitude}` +
|
||||
`&mlon=${coords.longitude}` +
|
||||
`#map=16/${coords.latitude}/${coords.longitude}`
|
||||
);
|
||||
};
|
||||
|
||||
export const createMapSiteLinkFromEvent = (event: MatrixEvent): string | null => {
|
||||
const content = event.getContent();
|
||||
const mLocation = content[M_LOCATION.name];
|
||||
if (mLocation !== undefined) {
|
||||
const uri = mLocation["uri"];
|
||||
if (uri !== undefined) {
|
||||
const geoCoords = parseGeoUri(uri);
|
||||
return geoCoords ? makeMapSiteLink(geoCoords) : null;
|
||||
}
|
||||
} else {
|
||||
const geoUri = content["geo_uri"];
|
||||
if (geoUri) {
|
||||
const geoCoords = parseGeoUri(geoUri);
|
||||
return geoCoords ? makeMapSiteLink(geoCoords) : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -15,8 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Map as MapLibreMap } from "maplibre-gl";
|
||||
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { createMap } from "./map";
|
||||
import { useMatrixClientContext } from "../../contexts/MatrixClientContext";
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Room,
|
||||
LocalNotificationSettings,
|
||||
ReceiptType,
|
||||
IMarkedUnreadEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { IndicatorIcon } from "@vector-im/compound-web";
|
||||
|
||||
@@ -28,6 +29,19 @@ import SettingsStore from "../settings/SettingsStore";
|
||||
import { NotificationLevel } from "../stores/notifications/NotificationLevel";
|
||||
import { doesRoomHaveUnreadMessages } from "../Unread";
|
||||
|
||||
// MSC2867 is not yet spec at time of writing. We read from both stable
|
||||
// and unstable prefixes and accept the risk that the format may change,
|
||||
// since the stable prefix is not actually defined yet.
|
||||
|
||||
/**
|
||||
* Unstable identifier for the marked_unread event, per MSC2867
|
||||
*/
|
||||
export const MARKED_UNREAD_TYPE_UNSTABLE = "com.famedly.marked_unread";
|
||||
/**
|
||||
* Stable identifier for the marked_unread event
|
||||
*/
|
||||
export const MARKED_UNREAD_TYPE_STABLE = "m.marked_unread";
|
||||
|
||||
export const deviceNotificationSettingsKeys = [
|
||||
"notificationsEnabled",
|
||||
"notificationBodyEnabled",
|
||||
@@ -74,6 +88,8 @@ export function localNotificationsAreSilenced(cli: MatrixClient): boolean {
|
||||
export async function clearRoomNotification(room: Room, client: MatrixClient): Promise<{} | undefined> {
|
||||
const lastEvent = room.getLastLiveEvent();
|
||||
|
||||
await setMarkedUnreadState(room, client, false);
|
||||
|
||||
try {
|
||||
if (lastEvent) {
|
||||
const receiptType = SettingsStore.getValue("sendReadReceipts", room.roomId)
|
||||
@@ -117,6 +133,39 @@ export function clearAllNotifications(client: MatrixClient): Promise<Array<{} |
|
||||
return Promise.all(receiptPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives the marked_unread state of the given room
|
||||
* @param room The room to check
|
||||
* @returns - The marked_unread state of the room, or undefined if no explicit state is set.
|
||||
*/
|
||||
export function getMarkedUnreadState(room: Room): boolean | undefined {
|
||||
const currentStateStable = room.getAccountData(MARKED_UNREAD_TYPE_STABLE)?.getContent<IMarkedUnreadEvent>()?.unread;
|
||||
const currentStateUnstable = room
|
||||
.getAccountData(MARKED_UNREAD_TYPE_UNSTABLE)
|
||||
?.getContent<IMarkedUnreadEvent>()?.unread;
|
||||
return currentStateStable ?? currentStateUnstable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the marked_unread state of the given room. This sets some room account data that indicates to
|
||||
* clients that the user considers this room to be 'unread', but without any actual notifications.
|
||||
*
|
||||
* @param room The room to set
|
||||
* @param client MatrixClient object to use
|
||||
* @param unread The new marked_unread state of the room
|
||||
*/
|
||||
export async function setMarkedUnreadState(room: Room, client: MatrixClient, unread: boolean): Promise<void> {
|
||||
// if there's no event, treat this as false as we don't need to send the flag to clear it if the event isn't there
|
||||
const currentState = getMarkedUnreadState(room);
|
||||
|
||||
if (Boolean(currentState) !== unread) {
|
||||
// Assuming MSC2867 passes FCP with no changes, we should update to start writing
|
||||
// the flag to the stable prefix (or both) and then ultimately use only the
|
||||
// stable prefix.
|
||||
await client.setRoomAccountData(room.roomId, MARKED_UNREAD_TYPE_UNSTABLE, { unread });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to transform a notification color to the what the Compound Icon Button
|
||||
* expects
|
||||
|
||||
Reference in New Issue
Block a user