Merge branch 'livekit' into fkwp/dev_build
This commit is contained in:
@@ -9,4 +9,4 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref
|
|||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" }
|
android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" }
|
||||||
maven_publish = { id = "com.vanniktech.maven.publish", version = "0.32.0" }
|
maven_publish = { id = "com.vanniktech.maven.publish", version = "0.31.0" }
|
||||||
@@ -27,7 +27,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mavenPublishing {
|
mavenPublishing {
|
||||||
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true)
|
publishToMavenCentral(SonatypeHost.S01, automaticRelease = true)
|
||||||
|
|
||||||
signAllPublications()
|
signAllPublications()
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { useAudioContext } from "../useAudioContext";
|
|||||||
import { ActiveCall } from "./InCallView";
|
import { ActiveCall } from "./InCallView";
|
||||||
import {
|
import {
|
||||||
flushPromises,
|
flushPromises,
|
||||||
|
mockEmitter,
|
||||||
mockMatrixRoom,
|
mockMatrixRoom,
|
||||||
mockMatrixRoomMember,
|
mockMatrixRoomMember,
|
||||||
mockRtcMembership,
|
mockRtcMembership,
|
||||||
@@ -129,6 +130,7 @@ function createGroupCallView(
|
|||||||
getMxcAvatarUrl: () => null,
|
getMxcAvatarUrl: () => null,
|
||||||
getCanonicalAlias: () => null,
|
getCanonicalAlias: () => null,
|
||||||
currentState: {
|
currentState: {
|
||||||
|
...mockEmitter(),
|
||||||
getJoinRule: () => JoinRule.Invite,
|
getJoinRule: () => JoinRule.Invite,
|
||||||
} as Partial<RoomState> as RoomState,
|
} as Partial<RoomState> as RoomState,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import {
|
|||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { type LivekitFocus, isLivekitFocus } from "matrix-js-sdk/lib/matrixrtc";
|
import { type LivekitFocus, isLivekitFocus } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
|
||||||
|
import { useTypedEventEmitterState } from "../useEvents";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the currently active (livekit) focus for a MatrixRTC session
|
* Gets the currently active (livekit) focus for a MatrixRTC session
|
||||||
* This logic is specific to livekit foci where the whole call must use one
|
* This logic is specific to livekit foci where the whole call must use one
|
||||||
@@ -22,38 +24,27 @@ import { type LivekitFocus, isLivekitFocus } from "matrix-js-sdk/lib/matrixrtc";
|
|||||||
export function useActiveLivekitFocus(
|
export function useActiveLivekitFocus(
|
||||||
rtcSession: MatrixRTCSession,
|
rtcSession: MatrixRTCSession,
|
||||||
): LivekitFocus | undefined {
|
): LivekitFocus | undefined {
|
||||||
const [activeFocus, setActiveFocus] = useState(() => {
|
const activeFocus = useTypedEventEmitterState(
|
||||||
const f = rtcSession.getActiveFocus();
|
rtcSession,
|
||||||
// Only handle foci with type="livekit" for now.
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
return !!f && isLivekitFocus(f) ? f : undefined;
|
useCallback(() => {
|
||||||
});
|
const f = rtcSession.getActiveFocus();
|
||||||
|
// Only handle foci with type="livekit" for now.
|
||||||
|
return !!f && isLivekitFocus(f) ? f : undefined;
|
||||||
|
}, [rtcSession]),
|
||||||
|
);
|
||||||
|
|
||||||
const onMembershipsChanged = useCallback(() => {
|
const prevActiveFocus = useRef(activeFocus);
|
||||||
const newActiveFocus = rtcSession.getActiveFocus();
|
useEffect(() => {
|
||||||
if (!!newActiveFocus && !isLivekitFocus(newActiveFocus)) return;
|
if (!deepCompare(activeFocus, prevActiveFocus.current)) {
|
||||||
if (!deepCompare(activeFocus, newActiveFocus)) {
|
|
||||||
const oldestMembership = rtcSession.getOldestMembership();
|
const oldestMembership = rtcSession.getOldestMembership();
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Got new active focus from membership: ${oldestMembership?.sender}/${oldestMembership?.deviceId}.
|
`Got new active focus from membership: ${oldestMembership?.sender}/${oldestMembership?.deviceId}.
|
||||||
Updating focus (focus switch) from ${JSON.stringify(activeFocus)} to ${JSON.stringify(newActiveFocus)}`,
|
Updated focus (focus switch) from ${JSON.stringify(prevActiveFocus.current)} to ${JSON.stringify(activeFocus)}`,
|
||||||
);
|
);
|
||||||
setActiveFocus(newActiveFocus);
|
prevActiveFocus.current = activeFocus;
|
||||||
}
|
}
|
||||||
}, [activeFocus, rtcSession]);
|
}, [activeFocus, rtcSession]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
rtcSession.on(
|
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
|
||||||
onMembershipsChanged,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (): void => {
|
|
||||||
rtcSession.off(
|
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
|
||||||
onMembershipsChanged,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return activeFocus;
|
return activeFocus;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2023, 2024 New Vector Ltd.
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Room, RoomEvent } from "matrix-js-sdk";
|
import { type Room, RoomEvent } from "matrix-js-sdk";
|
||||||
import { useState } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { useTypedEventEmitter } from "../useEvents";
|
import { useTypedEventEmitterState } from "../useEvents";
|
||||||
|
|
||||||
export function useRoomName(room: Room): string {
|
export function useRoomName(room: Room): string {
|
||||||
const [, setNumUpdates] = useState(0);
|
return useTypedEventEmitterState(
|
||||||
// Whenever the name changes, force an update
|
room,
|
||||||
useTypedEventEmitter(room, RoomEvent.Name, () => setNumUpdates((n) => n + 1));
|
RoomEvent.Name,
|
||||||
return room.name;
|
useCallback(() => room.name, [room]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback } from "react";
|
||||||
import { type RoomState, RoomStateEvent, type Room } from "matrix-js-sdk";
|
import {
|
||||||
|
type RoomState,
|
||||||
|
RoomStateEvent,
|
||||||
|
type Room,
|
||||||
|
RoomEvent,
|
||||||
|
} from "matrix-js-sdk";
|
||||||
|
|
||||||
import { useTypedEventEmitter } from "../useEvents";
|
import { useTypedEventEmitterState } from "../useEvents";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A React hook for values computed from room state.
|
* A React hook for values computed from room state.
|
||||||
@@ -16,14 +21,17 @@ import { useTypedEventEmitter } from "../useEvents";
|
|||||||
* @param f A mapping from the current room state to the computed value.
|
* @param f A mapping from the current room state to the computed value.
|
||||||
* @returns The computed value.
|
* @returns The computed value.
|
||||||
*/
|
*/
|
||||||
export const useRoomState = <T>(room: Room, f: (state: RoomState) => T): T => {
|
export function useRoomState<T>(room: Room, f: (state: RoomState) => T): T {
|
||||||
const [numUpdates, setNumUpdates] = useState(0);
|
// TODO: matrix-js-sdk says that Room.currentState is deprecated, but it's not
|
||||||
useTypedEventEmitter(
|
// clear how to reactively track the current state of the room without it
|
||||||
|
const currentState = useTypedEventEmitterState(
|
||||||
room,
|
room,
|
||||||
RoomStateEvent.Update,
|
RoomEvent.CurrentStateUpdated,
|
||||||
useCallback(() => setNumUpdates((n) => n + 1), [setNumUpdates]),
|
useCallback(() => room.currentState, [room]),
|
||||||
);
|
);
|
||||||
// We want any change to the update counter to trigger an update here
|
return useTypedEventEmitterState(
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
currentState,
|
||||||
return useMemo(() => f(room.currentState), [room, f, numUpdates]);
|
RoomStateEvent.Update,
|
||||||
};
|
useCallback(() => f(currentState), [f, currentState]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
88
src/useEvents.test.tsx
Normal file
88
src/useEvents.test.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { type FC, useEffect, useState } from "react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { TypedEventEmitter } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
import { useTypedEventEmitterState } from "./useEvents";
|
||||||
|
|
||||||
|
class TestEmitter extends TypedEventEmitter<"change", { change: () => void }> {
|
||||||
|
private state = 1;
|
||||||
|
public readonly getState = (): number => this.state;
|
||||||
|
public readonly getNegativeState = (): number => -this.state;
|
||||||
|
public readonly setState = (value: number): void => {
|
||||||
|
this.state = value;
|
||||||
|
this.emit("change");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("useTypedEventEmitterState reacts to events", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const emitter = new TestEmitter();
|
||||||
|
|
||||||
|
const Test: FC = () => {
|
||||||
|
const value = useTypedEventEmitterState(
|
||||||
|
emitter,
|
||||||
|
"change",
|
||||||
|
emitter.getState,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={() => emitter.setState(2)}>Change value</button>
|
||||||
|
<div>Value is {value}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Test />);
|
||||||
|
screen.getByText("Value is 1");
|
||||||
|
await user.click(screen.getByText("Change value"));
|
||||||
|
screen.getByText("Value is 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("useTypedEventEmitterState reacts to changes made by an effect mounted on the same render", () => {
|
||||||
|
const emitter = new TestEmitter();
|
||||||
|
|
||||||
|
const Test: FC = () => {
|
||||||
|
useEffect(() => emitter.setState(2), []);
|
||||||
|
const value = useTypedEventEmitterState(
|
||||||
|
emitter,
|
||||||
|
"change",
|
||||||
|
emitter.getState,
|
||||||
|
);
|
||||||
|
return `Value is ${value}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Test />);
|
||||||
|
screen.getByText("Value is 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("useTypedEventEmitterState reacts to changes in getState", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const emitter = new TestEmitter();
|
||||||
|
|
||||||
|
const Test: FC = () => {
|
||||||
|
const [fn, setFn] = useState(() => emitter.getState);
|
||||||
|
const value = useTypedEventEmitterState(emitter, "change", fn);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={() => setFn(() => emitter.getNegativeState)}>
|
||||||
|
Change getState
|
||||||
|
</button>
|
||||||
|
<div>Value is {value}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Test />);
|
||||||
|
screen.getByText("Value is 1");
|
||||||
|
await user.click(screen.getByText("Change getState"));
|
||||||
|
screen.getByText("Value is -1");
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Listener,
|
Listener,
|
||||||
@@ -13,7 +13,9 @@ import type {
|
|||||||
TypedEventEmitter,
|
TypedEventEmitter,
|
||||||
} from "matrix-js-sdk/lib/models/typed-event-emitter";
|
} from "matrix-js-sdk/lib/models/typed-event-emitter";
|
||||||
|
|
||||||
// Shortcut for registering a listener on an EventTarget
|
/**
|
||||||
|
* Shortcut for registering a listener on an EventTarget.
|
||||||
|
*/
|
||||||
export function useEventTarget<T extends Event>(
|
export function useEventTarget<T extends Event>(
|
||||||
target: EventTarget | null | undefined,
|
target: EventTarget | null | undefined,
|
||||||
eventType: string,
|
eventType: string,
|
||||||
@@ -33,7 +35,9 @@ export function useEventTarget<T extends Event>(
|
|||||||
}, [target, eventType, listener, options]);
|
}, [target, eventType, listener, options]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shortcut for registering a listener on a TypedEventEmitter
|
/**
|
||||||
|
* Shortcut for registering a listener on a TypedEventEmitter.
|
||||||
|
*/
|
||||||
export function useTypedEventEmitter<
|
export function useTypedEventEmitter<
|
||||||
Events extends string,
|
Events extends string,
|
||||||
Arguments extends ListenerMap<Events>,
|
Arguments extends ListenerMap<Events>,
|
||||||
@@ -50,3 +54,33 @@ export function useTypedEventEmitter<
|
|||||||
};
|
};
|
||||||
}, [emitter, eventType, listener]);
|
}, [emitter, eventType, listener]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reactively tracks a value which is recalculated whenever the provided event
|
||||||
|
* emitter emits an event. This is useful for bridging state from matrix-js-sdk
|
||||||
|
* into React.
|
||||||
|
*/
|
||||||
|
export function useTypedEventEmitterState<
|
||||||
|
Events extends string,
|
||||||
|
Arguments extends ListenerMap<Events>,
|
||||||
|
T extends Events,
|
||||||
|
State,
|
||||||
|
>(
|
||||||
|
emitter: TypedEventEmitter<Events, Arguments>,
|
||||||
|
eventType: T,
|
||||||
|
getState: () => State,
|
||||||
|
): State {
|
||||||
|
const subscribe = useCallback(
|
||||||
|
(onChange: () => void) => {
|
||||||
|
emitter.on(eventType, onChange as Listener<Events, Arguments, T>);
|
||||||
|
return (): void => {
|
||||||
|
emitter.off(eventType, onChange as Listener<Events, Arguments, T>);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[emitter, eventType],
|
||||||
|
);
|
||||||
|
// See the React docs for useSyncExternalStore; given that we're trying to
|
||||||
|
// bridge state from an external source into React, using this hook is exactly
|
||||||
|
// what React recommends.
|
||||||
|
return useSyncExternalStore(subscribe, getState);
|
||||||
|
}
|
||||||
|
|||||||
51
src/useLocalStorage.test.tsx
Normal file
51
src/useLocalStorage.test.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { type FC, useEffect, useState } from "react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
|
import {
|
||||||
|
setLocalStorageItemReactive,
|
||||||
|
useLocalStorage,
|
||||||
|
} from "./useLocalStorage";
|
||||||
|
|
||||||
|
test("useLocalStorage reacts to changes made by an effect mounted on the same render", () => {
|
||||||
|
localStorage.clear();
|
||||||
|
const Test: FC = () => {
|
||||||
|
useEffect(() => setLocalStorageItemReactive("my-value", "Hello!"), []);
|
||||||
|
const [myValue] = useLocalStorage("my-value");
|
||||||
|
return myValue;
|
||||||
|
};
|
||||||
|
render(<Test />);
|
||||||
|
screen.getByText("Hello!");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("useLocalStorage reacts to key changes", async () => {
|
||||||
|
localStorage.clear();
|
||||||
|
localStorage.setItem("value-1", "1");
|
||||||
|
localStorage.setItem("value-2", "2");
|
||||||
|
|
||||||
|
const Test: FC = () => {
|
||||||
|
const [key, setKey] = useState("value-1");
|
||||||
|
const [value] = useLocalStorage(key);
|
||||||
|
if (key !== `value-${value}`) throw new Error("Value is out of sync");
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={() => setKey("value-2")}>Switch keys</button>
|
||||||
|
<div>Value is: {value}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<Test />);
|
||||||
|
|
||||||
|
screen.getByText("Value is: 1");
|
||||||
|
await user.click(screen.getByRole("button", { name: "Switch keys" }));
|
||||||
|
screen.getByText("Value is: 2");
|
||||||
|
});
|
||||||
@@ -5,50 +5,44 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import EventEmitter from "events";
|
import { useCallback } from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { TypedEventEmitter } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
import { useTypedEventEmitterState } from "./useEvents";
|
||||||
|
|
||||||
type LocalStorageItem = ReturnType<typeof localStorage.getItem>;
|
type LocalStorageItem = ReturnType<typeof localStorage.getItem>;
|
||||||
|
|
||||||
// Bus to notify other useLocalStorage consumers when an item is changed
|
// Bus to notify other useLocalStorage consumers when an item is changed
|
||||||
export const localStorageBus = new EventEmitter();
|
export const localStorageBus = new TypedEventEmitter<
|
||||||
|
string,
|
||||||
|
{ [key: string]: () => void }
|
||||||
|
>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like useState, but reads from and persists the value to localStorage
|
* Like useState, but reads from and persists the value to localStorage
|
||||||
* This hook will not update when we write to localStorage.setItem(key, value) directly.
|
* This hook will not update when we write to localStorage.setItem(key, value) directly.
|
||||||
* For the hook to react either use the returned setter or `setLocalStorageItemReactive`.
|
* For the hook to react either use the returned setter or `setLocalStorageItemReactive`.
|
||||||
*/
|
*/
|
||||||
export const useLocalStorage = (
|
export function useLocalStorage(
|
||||||
key: string,
|
key: string,
|
||||||
): [LocalStorageItem, (value: string) => void] => {
|
): [LocalStorageItem, (value: string) => void] {
|
||||||
const [value, setValue] = useState<LocalStorageItem>(() =>
|
const value = useTypedEventEmitterState(
|
||||||
localStorage.getItem(key),
|
localStorageBus,
|
||||||
|
key,
|
||||||
|
useCallback(() => localStorage.getItem(key), [key]),
|
||||||
|
);
|
||||||
|
const setValue = useCallback(
|
||||||
|
(newValue: string) => setLocalStorageItemReactive(key, newValue),
|
||||||
|
[key],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
return [value, setValue];
|
||||||
localStorageBus.on(key, setValue);
|
}
|
||||||
return (): void => {
|
|
||||||
localStorageBus.off(key, setValue);
|
|
||||||
};
|
|
||||||
}, [key, setValue]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
value,
|
|
||||||
useCallback(
|
|
||||||
(newValue: string) => {
|
|
||||||
setValue(newValue);
|
|
||||||
localStorage.setItem(key, newValue);
|
|
||||||
localStorageBus.emit(key, newValue);
|
|
||||||
},
|
|
||||||
[key, setValue],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setLocalStorageItemReactive = (
|
export const setLocalStorageItemReactive = (
|
||||||
key: string,
|
key: string,
|
||||||
value: string,
|
value: string,
|
||||||
): void => {
|
): void => {
|
||||||
localStorage.setItem(key, value);
|
localStorage.setItem(key, value);
|
||||||
localStorageBus.emit(key, value);
|
localStorageBus.emit(key);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,33 +10,31 @@ import {
|
|||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { useEffect, useState } from "react";
|
import { TypedEventEmitter } from "matrix-js-sdk";
|
||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
|
import { useTypedEventEmitterState } from "./useEvents";
|
||||||
|
|
||||||
|
const dummySession = new TypedEventEmitter();
|
||||||
|
|
||||||
export function useMatrixRTCSessionJoinState(
|
export function useMatrixRTCSessionJoinState(
|
||||||
rtcSession: MatrixRTCSession | undefined,
|
rtcSession: MatrixRTCSession | undefined,
|
||||||
): boolean {
|
): boolean {
|
||||||
const [, setNumUpdates] = useState(0);
|
// React doesn't allow you to run a hook conditionally, so we have to plug in
|
||||||
|
// a dummy event emitter in case there is no rtcSession yet
|
||||||
|
const isJoined = useTypedEventEmitterState(
|
||||||
|
rtcSession ?? dummySession,
|
||||||
|
MatrixRTCSessionEvent.JoinStateChanged,
|
||||||
|
useCallback(() => rtcSession?.isJoined() ?? false, [rtcSession]),
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rtcSession !== undefined) {
|
logger.info(
|
||||||
const onJoinStateChanged = (isJoined: boolean): void => {
|
`Session in room ${rtcSession?.room.roomId} changed to ${
|
||||||
logger.info(
|
isJoined ? "joined" : "left"
|
||||||
`Session in room ${rtcSession.room.roomId} changed to ${
|
}`,
|
||||||
isJoined ? "joined" : "left"
|
);
|
||||||
}`,
|
}, [rtcSession, isJoined]);
|
||||||
);
|
|
||||||
setNumUpdates((n) => n + 1); // Force an update
|
|
||||||
};
|
|
||||||
rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, onJoinStateChanged);
|
|
||||||
|
|
||||||
return (): void => {
|
return isJoined;
|
||||||
rtcSession.off(
|
|
||||||
MatrixRTCSessionEvent.JoinStateChanged,
|
|
||||||
onJoinStateChanged,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [rtcSession]);
|
|
||||||
|
|
||||||
return rtcSession?.isJoined() ?? false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,39 +5,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
|
||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import { useTypedEventEmitterState } from "./useEvents";
|
||||||
|
|
||||||
export function useMatrixRTCSessionMemberships(
|
export function useMatrixRTCSessionMemberships(
|
||||||
rtcSession: MatrixRTCSession,
|
rtcSession: MatrixRTCSession,
|
||||||
): CallMembership[] {
|
): CallMembership[] {
|
||||||
const [memberships, setMemberships] = useState(rtcSession.memberships);
|
return useTypedEventEmitterState(
|
||||||
|
rtcSession,
|
||||||
const onMembershipsChanged = useCallback(() => {
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
logger.info(
|
useCallback(() => rtcSession.memberships, [rtcSession]),
|
||||||
`Memberships changed for call in room ${rtcSession.room.roomId} (${rtcSession.memberships.length} members)`,
|
);
|
||||||
);
|
|
||||||
setMemberships(rtcSession.memberships);
|
|
||||||
}, [rtcSession]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
rtcSession.on(
|
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
|
||||||
onMembershipsChanged,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (): void => {
|
|
||||||
rtcSession.off(
|
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
|
||||||
onMembershipsChanged,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}, [rtcSession, onMembershipsChanged]);
|
|
||||||
|
|
||||||
return memberships;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2023, 2024 New Vector Ltd.
|
Copyright 2023-2025 New Vector Ltd.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
||||||
|
|
||||||
import { useEventTarget } from "./useEvents";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React hook that tracks whether the given media query matches.
|
* React hook that tracks whether the given media query matches.
|
||||||
@@ -15,14 +13,13 @@ import { useEventTarget } from "./useEvents";
|
|||||||
export function useMediaQuery(query: string): boolean {
|
export function useMediaQuery(query: string): boolean {
|
||||||
const mediaQuery = useMemo(() => window.matchMedia(query), [query]);
|
const mediaQuery = useMemo(() => window.matchMedia(query), [query]);
|
||||||
|
|
||||||
const [numChanges, setNumChanges] = useState(0);
|
const subscribe = useCallback(
|
||||||
useEventTarget(
|
(onChange: () => void) => {
|
||||||
mediaQuery,
|
mediaQuery.addEventListener("change", onChange);
|
||||||
"change",
|
return (): void => mediaQuery.removeEventListener("change", onChange);
|
||||||
useCallback(() => setNumChanges((n) => n + 1), [setNumChanges]),
|
},
|
||||||
|
[mediaQuery],
|
||||||
);
|
);
|
||||||
|
const getState = useCallback(() => mediaQuery.matches, [mediaQuery]);
|
||||||
// We want any change to the update counter to trigger an update here
|
return useSyncExternalStore(subscribe, getState);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
return useMemo(() => mediaQuery.matches, [mediaQuery, numChanges]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ interface EmitterMock<T> {
|
|||||||
removeListener: () => T;
|
removeListener: () => T;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockEmitter<T>(): EmitterMock<T> {
|
export function mockEmitter<T>(): EmitterMock<T> {
|
||||||
return {
|
return {
|
||||||
on(): T {
|
on(): T {
|
||||||
return this as T;
|
return this as T;
|
||||||
|
|||||||
Reference in New Issue
Block a user