mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
Say outright whether the profile is ours to change
Whether to offer the profile settings was inferred from whether the host could close Element Call. For a component with no host bridge — the default — nothing could, so an embedded Element Call let the user edit the profile of an account that belongs to the host application. `HostBridge.supportsProfileChanges` states it directly: true standalone, where Element Call signed the user in itself; false for a widget's host and for anything embedding the component (which sets it itself, since the client it hands over is its own). The profile tab and the profile shortcut follow that. What a host's ability to close us still decides — what to show after the call ends — is a question about who owns our lifetime, and stays keyed on `close`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
f0359259f9
commit
7cd21476f7
@@ -10,7 +10,7 @@ import { NEVER, Subject } from "rxjs";
|
|||||||
import {
|
import {
|
||||||
type DeviceMuteRequest,
|
type DeviceMuteRequest,
|
||||||
type DeviceMuteState,
|
type DeviceMuteState,
|
||||||
type HostBridge,
|
type ElementCallHostBridge,
|
||||||
type HostRequest,
|
type HostRequest,
|
||||||
} from "../index";
|
} from "../index";
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
* so that the harness can watch both directions of the conversation between
|
* so that the harness can watch both directions of the conversation between
|
||||||
* Element Call and its host.
|
* Element Call and its host.
|
||||||
*/
|
*/
|
||||||
export interface DevHostBridge extends HostBridge {
|
export interface DevHostBridge extends ElementCallHostBridge {
|
||||||
/** Tells Element Call the host has changed theme. */
|
/** Tells Element Call the host has changed theme. */
|
||||||
requestTheme(name: string): void;
|
requestTheme(name: string): void;
|
||||||
/** Tells Element Call to leave the call. */
|
/** Tells Element Call to leave the call. */
|
||||||
|
|||||||
+21
-2
@@ -100,6 +100,14 @@ export {
|
|||||||
*/
|
*/
|
||||||
export type ElementCallConfiguration = Partial<UrlParams>;
|
export type ElementCallConfiguration = Partial<UrlParams>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What a host embedding Element Call implements to talk to it. This is the
|
||||||
|
* {@link HostBridge} less what Element Call already knows about such a host:
|
||||||
|
* the account is the host's, since the client is, so the profile is not
|
||||||
|
* Element Call's to change.
|
||||||
|
*/
|
||||||
|
export type ElementCallHostBridge = Omit<HostBridge, "supportsProfileChanges">;
|
||||||
|
|
||||||
export interface ElementCallProps {
|
export interface ElementCallProps {
|
||||||
/**
|
/**
|
||||||
* The client to place the call with. Element Call does not authenticate
|
* The client to place the call with. Element Call does not authenticate
|
||||||
@@ -132,7 +140,7 @@ export interface ElementCallProps {
|
|||||||
* joined or hung up, to be asked to keep the call on screen, and so on.
|
* joined or hung up, to be asked to keep the call on screen, and so on.
|
||||||
* Without one, Element Call assumes it has no host to talk to.
|
* Without one, Element Call assumes it has no host to talk to.
|
||||||
*/
|
*/
|
||||||
hostBridge?: HostBridge;
|
hostBridge?: ElementCallHostBridge;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,8 +189,19 @@ export const ElementCall: FC<ElementCallProps> = ({
|
|||||||
roomId,
|
roomId,
|
||||||
intent = UserIntent.JoinExistingCall,
|
intent = UserIntent.JoinExistingCall,
|
||||||
config,
|
config,
|
||||||
hostBridge = nullHostBridge,
|
hostBridge: suppliedHostBridge = nullHostBridge,
|
||||||
}): ReactNode => {
|
}): ReactNode => {
|
||||||
|
// Whatever the host says or does not say, the account is its own: it signed
|
||||||
|
// the user in and handed us the client. So Element Call never offers to edit
|
||||||
|
// the profile from inside a component.
|
||||||
|
const hostBridge = useMemo(
|
||||||
|
(): HostBridge => ({
|
||||||
|
...suppliedHostBridge,
|
||||||
|
supportsProfileChanges: false,
|
||||||
|
}),
|
||||||
|
[suppliedHostBridge],
|
||||||
|
);
|
||||||
|
|
||||||
// The container is what Element Call decorates and portals into, so nothing
|
// The container is what Element Call decorates and portals into, so nothing
|
||||||
// inside can render until we have it.
|
// inside can render until we have it.
|
||||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
|
|||||||
@@ -247,6 +247,11 @@ describe("createWidgetHostBridge", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("does not offer profile changes, since the host signed the user in", () => {
|
||||||
|
const bridge = createWidgetHostBridge(mockWidget({}));
|
||||||
|
expect(bridge.supportsProfileChanges).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
describe("supportsReactions", () => {
|
describe("supportsReactions", () => {
|
||||||
const capabilities = [
|
const capabilities = [
|
||||||
"org.matrix.msc2762.send.event:m.reaction",
|
"org.matrix.msc2762.send.event:m.reaction",
|
||||||
@@ -278,6 +283,10 @@ describe("nullHostBridge", () => {
|
|||||||
expect(nullHostBridge.close).toBeUndefined();
|
expect(nullHostBridge.close).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("supports profile changes, since Element Call signed the user in itself", () => {
|
||||||
|
expect(nullHostBridge.supportsProfileChanges).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("offers no media download, so Element Call uses its own client", () => {
|
test("offers no media download, so Element Call uses its own client", () => {
|
||||||
expect(nullHostBridge.downloadMedia).toBeUndefined();
|
expect(nullHostBridge.downloadMedia).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|||||||
+15
-1
@@ -96,8 +96,16 @@ export interface HostBridge {
|
|||||||
/** The host wants to change, or read back, the device mute state. */
|
/** The host wants to change, or read back, the device mute state. */
|
||||||
deviceMute$: Observable<HostRequest<DeviceMuteRequest, DeviceMuteState>>;
|
deviceMute$: Observable<HostRequest<DeviceMuteRequest, DeviceMuteState>>;
|
||||||
|
|
||||||
// What the host is capable of.
|
// What the host is, and is capable of.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Element Call may offer to change the user's profile — their
|
||||||
|
* display name and avatar. Only when the account is Element Call's own,
|
||||||
|
* which is to say standalone: a widget's host and an application embedding
|
||||||
|
* Element Call both signed the user in themselves, so the profile is theirs
|
||||||
|
* to manage and Element Call must not offer to edit it.
|
||||||
|
*/
|
||||||
|
readonly supportsProfileChanges: boolean;
|
||||||
/** Whether the host permits Element Call to send and receive reactions. */
|
/** Whether the host permits Element Call to send and receive reactions. */
|
||||||
readonly supportsReactions: boolean;
|
readonly supportsReactions: boolean;
|
||||||
/**
|
/**
|
||||||
@@ -122,6 +130,9 @@ export const nullHostBridge: HostBridge = {
|
|||||||
join$: NEVER,
|
join$: NEVER,
|
||||||
hangUp$: NEVER,
|
hangUp$: NEVER,
|
||||||
deviceMute$: NEVER,
|
deviceMute$: NEVER,
|
||||||
|
// Standalone, the account is Element Call's own: it signed the user in, so
|
||||||
|
// it may offer to change the profile.
|
||||||
|
supportsProfileChanges: true,
|
||||||
// Standalone Element Call reaches the homeserver itself, so nothing is
|
// Standalone Element Call reaches the homeserver itself, so nothing is
|
||||||
// withholding these from it.
|
// withholding these from it.
|
||||||
supportsReactions: true,
|
supportsReactions: true,
|
||||||
@@ -176,6 +187,9 @@ export function createWidgetHostBridge(widget: WidgetHelpers): HostBridge {
|
|||||||
join$: requests(ElementWidgetActions.JoinCall),
|
join$: requests(ElementWidgetActions.JoinCall),
|
||||||
hangUp$: requests(ElementWidgetActions.HangupCall),
|
hangUp$: requests(ElementWidgetActions.HangupCall),
|
||||||
deviceMute$: requests(ElementWidgetActions.DeviceMute),
|
deviceMute$: requests(ElementWidgetActions.DeviceMute),
|
||||||
|
// The client we are a widget of signed the user in, so the profile is its
|
||||||
|
// to manage
|
||||||
|
supportsProfileChanges: false,
|
||||||
// Element Call needs the host's permission to send reactions on its behalf.
|
// Element Call needs the host's permission to send reactions on its behalf.
|
||||||
// Read on access rather than up front: the widget API negotiates its
|
// Read on access rather than up front: the widget API negotiates its
|
||||||
// capabilities asynchronously, and the bridge is built before that settles.
|
// capabilities asynchronously, and the bridge is built before that settles.
|
||||||
|
|||||||
@@ -116,10 +116,8 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
const hostBridge = useHostBridge();
|
const hostBridge = useHostBridge();
|
||||||
// A host that can close us is a host that decides when we stop existing, so
|
// A host that can close us is a host that decides when we stop existing, so
|
||||||
// we neither show our own post-call screens nor assume we have time to
|
// we neither show our own post-call screens nor assume we have time to
|
||||||
// finish what we are doing.
|
// finish what we are doing. (Whose account the user's is, by contrast, is
|
||||||
// TODO: this reads a capability as a proxy for who owns our lifetime. Worth
|
// stated outright: see `HostBridge.supportsProfileChanges`.)
|
||||||
// finding a more direct way to express it — see the guidance in UrlParams.ts
|
|
||||||
// on naming behaviours rather than situations.
|
|
||||||
const hostControlsLifetime = hostBridge.close !== undefined;
|
const hostControlsLifetime = hostBridge.close !== undefined;
|
||||||
|
|
||||||
const muteAllAudio = useBehavior(muteAllAudio$);
|
const muteAllAudio = useBehavior(muteAllAudio$);
|
||||||
|
|||||||
@@ -329,11 +329,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
|
|
||||||
const openProfile = useMemo(
|
const openProfile = useMemo(
|
||||||
() =>
|
() =>
|
||||||
// A host that can dismiss us is a host that owns the user's account, so
|
// The profile is only ours to edit when the account is ours
|
||||||
// their profile is not ours to edit.
|
hostBridge.supportsProfileChanges
|
||||||
// TODO: another use of the close capability as a proxy — see the note in
|
|
||||||
// GroupCallView.
|
|
||||||
hostBridge.close === undefined
|
|
||||||
? (): void => {
|
? (): void => {
|
||||||
setSettingsTab("profile");
|
setSettingsTab("profile");
|
||||||
setSettingsOpen(true);
|
setSettingsOpen(true);
|
||||||
|
|||||||
@@ -235,9 +235,8 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tabs = [audioTab, videoTab];
|
const tabs = [audioTab, videoTab];
|
||||||
// A host that can dismiss us is a host that owns the user's account, so their
|
// The profile is only ours to edit when the account is ours
|
||||||
// profile is not ours to edit.
|
if (hostBridge.supportsProfileChanges) tabs.push(profileTab);
|
||||||
if (hostBridge.close === undefined) tabs.push(profileTab);
|
|
||||||
tabs.push(preferencesTab);
|
tabs.push(preferencesTab);
|
||||||
if (isRageshakeAvailable || import.meta.env.VITE_PACKAGE === "full") {
|
if (isRageshakeAvailable || import.meta.env.VITE_PACKAGE === "full") {
|
||||||
// for full package we want to show the analytics consent checkbox
|
// for full package we want to show the analytics consent checkbox
|
||||||
|
|||||||
Reference in New Issue
Block a user