feat(status): make the set-status menu work

feat(user): optimise get() function for react
qol(todos): update todos
This commit is contained in:
Alois 2026-08-10 13:48:32 +02:00
commit 1c90a0c859
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
30 changed files with 572 additions and 324 deletions

View file

@ -63,6 +63,7 @@ export default function InviteButton({
<Wrapper
key={contact.UserId}
userId={contact.UserId}
fields={["Display"]}
loading={<div>Loading...</div>}
component={(user) => (
<Button

View file

@ -23,6 +23,7 @@ export default function InvitePopup({
return (
<Wrapper
userId={user}
fields={["Avatar", "Display"]}
loading={null}
component={(user) => (
<Dialog open={open} onOpenChange={setOpen}>

View file

@ -16,13 +16,15 @@ import {
} from "../../store";
import { Track, type Participant } from "livekit-client";
import { useEffect, useRef, useState } from "react";
import { useUser, type User } from "@tensamin/user/context";
import { type SelectedUser, useUserFields } from "@tensamin/user/context";
import { useIsSpeaking } from "../../speakingState";
import VideoViewer from "../videoViewer";
import { HeadphoneOff, MicOff, Monitor, Plus, Shield } from "lucide-react";
import ContextMenu from "./contextMenu";
import { useStorage } from "@tensamin/storage/context";
const USER_FIELDS = ["UserId", "Avatar", "Display"] as const;
function getTrackPublicationBySource(
participant: Participant | undefined,
source: Track.Source,
@ -113,7 +115,7 @@ function Overlay({
participant,
}: {
type: "user" | "stream";
user: User;
user: SelectedUser<typeof USER_FIELDS>;
participant: Participant;
}) {
const isAdmin = getRoomMetadata()?.admins.includes(user.UserId) === true;
@ -163,12 +165,16 @@ export default function Base({
fill?: boolean;
flush?: boolean;
}) {
const { get } = useUser();
const { load } = useStorage();
const focusedParticipantId = useCall((state) => state.focusedParticipantId);
const view = useCall((state) => state.view);
const [user, setUser] = useState<User | null>(null);
const participantId = Number(participant?.identity);
const validParticipantId =
participant && Number.isInteger(participantId) && participantId > 0
? participantId
: null;
const { data: user } = useUserFields(validParticipantId, USER_FIELDS);
const [avatarBackgroundColor, setAvatarBackgroundColor] = useState<
string | undefined
>(undefined);
@ -191,31 +197,6 @@ export default function Base({
load("user_id").then(setOwnId);
}, [load]);
useEffect(() => {
const participantId = Number(participant?.identity);
if (
!participant ||
!Number.isInteger(participantId) ||
participantId <= 0
) {
setUser(null);
return;
}
let active = true;
void get(participantId).then((nextUser) => {
if (active) {
setUser(nextUser);
}
});
return () => {
active = false;
};
}, [participant, get]);
useEffect(() => {
if (type !== "user" || !user?.Avatar) {
setAvatarBackgroundColor(undefined);

View file

@ -5,7 +5,6 @@ import {
ContextMenuSeparator,
Slider,
} from "@methanium/ui";
import type { User } from "@tensamin/user/context";
import { setParticipantCameraDisabled, useCall } from "../../store";
import { useState } from "react";
@ -23,7 +22,7 @@ export default function ContextMenu({
user,
ownId,
}: {
user: User;
user: Readonly<{ UserId: number }>;
ownId: number;
}) {
const [muted, setMuted] = useState(false);

View file

@ -14,10 +14,12 @@ import {
useSidebar,
} from "@methanium/ui";
import { ScreenShareOff } from "lucide-react";
import { useUser, type User } from "@tensamin/user/context";
import { useUserFields } from "@tensamin/user/context";
import { useIsSpeaking, useLastSpeakingParticipantId } from "../speakingState";
import { getAverageImageColor } from "./modals/base";
const USER_FIELDS = ["Avatar", "Display"] as const;
function getTrackPublicationBySource(
participant: Participant | undefined,
source: Track.Source,
@ -62,10 +64,12 @@ function MobileCallPill({
callId: string;
}) {
const { setOpenMobile } = useSidebar();
const { get } = useUser();
const lastSpeakingParticipantId = useLastSpeakingParticipantId();
const isSpeaking = useIsSpeaking(lastSpeakingParticipantId ?? -1);
const [lastSpeakingUser, setLastSpeakingUser] = useState<User | null>(null);
const { data: lastSpeakingUser } = useUserFields(
lastSpeakingParticipantId,
USER_FIELDS,
);
const [avatarBackgroundColor, setAvatarBackgroundColor] = useState<
string | undefined
>(undefined);
@ -83,25 +87,6 @@ function MobileCallPill({
const [coords, setCoords] = useState(initialCoords);
const [isDragging, setIsDragging] = useState(false);
useEffect(() => {
if (lastSpeakingParticipantId == null) {
setLastSpeakingUser(null);
return;
}
let mounted = true;
void get(lastSpeakingParticipantId).then((user) => {
if (mounted) {
setLastSpeakingUser(user);
}
});
return () => {
mounted = false;
};
}, [get, lastSpeakingParticipantId]);
useEffect(() => {
if (!lastSpeakingUser?.Avatar) {
setAvatarBackgroundColor(undefined);

View file

@ -1,4 +1,4 @@
import { useUser, type User } from "@tensamin/user/context";
import { useUserFields } from "@tensamin/user/context";
import { useCall, getRoom } from "../store";
import { useEffect, useState } from "react";
import { useStorage } from "@tensamin/storage/context";
@ -11,8 +11,43 @@ import {
TooltipTrigger,
} from "@methanium/ui";
const USER_FIELDS = ["Avatar", "Display"] as const;
function ParticipantAvatar({
userId,
portalContainer,
}: {
userId: number;
portalContainer?: HTMLElement;
}) {
const { data: user } = useUserFields(userId, USER_FIELDS);
if (!user) return null;
return (
<div className="-ml-4">
<Tooltip>
<TooltipTrigger
render={
<Avatar className="size-7">
<AvatarImage src={user.Avatar} />
<AvatarFallback className="text-xs">
{user.Display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
}
/>
<TooltipContent
side="bottom"
portalProps={{ container: portalContainer }}
>
{user.Display}
</TooltipContent>
</Tooltip>
</div>
);
}
export default function TopBar() {
const { get } = useUser();
const { load } = useStorage();
const room = getRoom();
const screenRef = useCall((state) => state.screenRef);
@ -31,65 +66,23 @@ export default function TopBar() {
: null;
},
).filter((participantId): participantId is number => participantId != null);
const userIdsKey = userIds.join(",");
const [users, setUsers] = useState<User[]>([]);
const [ownId, setOwnId] = useState<number>();
useEffect(() => {
let active = true;
const ids = userIdsKey === "" ? [] : userIdsKey.split(",").map(Number);
void load("user_id").then(setOwnId);
}, [load]);
void Promise.all(ids.map((id) => get(id)))
.then(async (users) => {
if (!active) {
return;
}
const ownId = await load("user_id");
const ownUser = await get(ownId);
setUsers([ownUser, ...users]);
})
.catch(async () => {
if (!active) {
return;
}
const ownId = await load("user_id");
const ownUser = await get(ownId);
setUsers([ownUser]);
});
return () => {
active = false;
};
}, [get, userIdsKey, load]);
const participantIds = ownId === undefined ? userIds : [ownId, ...userIds];
return (
<div className="w-full flex justify-between h-12">
<div className="flex gap-1 m-3 ml-7">
{users.map((user) => (
<div key={user.UserId} className="-ml-4">
<Tooltip>
<TooltipTrigger
render={
<Avatar className="size-7">
<AvatarImage src={user.Avatar} />
<AvatarFallback className="text-xs">
{user.Display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
}
/>
<TooltipContent
side="bottom"
portalProps={{ container: portalContainer }}
>
{user.Display}
</TooltipContent>
</Tooltip>
</div>
{participantIds.map((userId) => (
<ParticipantAvatar
key={userId}
userId={userId}
portalContainer={portalContainer}
/>
))}
</div>
</div>

View file

@ -70,7 +70,6 @@ type SendFn = (
data: Record<string, unknown>,
) => Promise<{ data: unknown }>;
type LoadFn = (key: string) => Promise<unknown>;
type GetUserFn = (userId: number) => Promise<{ PublicKey: string }>;
type RemoteVideoTrackSelector = Track.Kind | Track.Source;
type Runtime = {
@ -80,7 +79,7 @@ type Runtime = {
}) => Promise<void>;
send: SendFn;
load: LoadFn;
getUser: GetUserFn;
getPublicKey: (userId: number) => Promise<string>;
};
let _keyProvider: ExternalE2EEKeyProvider | null = null;
@ -671,9 +670,7 @@ export async function sendCallInvite(userId: number) {
throw new Error("Cannot send call invite without an active call.");
}
const remotePublicKey = await runtime
.getUser(userId)
.then((data) => data.PublicKey);
const remotePublicKey = await runtime.getPublicKey(userId);
const secretId = deriveCallSecretId(callId);
const wrapped = await wrapCallSecret({
callSecret,
@ -1255,7 +1252,8 @@ export function useInitializeCall() {
navigate,
send: send as SendFn,
load: load as LoadFn,
getUser: get as GetUserFn,
getPublicKey: (userId) =>
get(userId, ["PublicKey"]).then((user) => user.PublicKey),
});
}, [get, load, navigate, send]);

View file

@ -1,54 +1,23 @@
import { useEffect, useState } from "react";
import { useCall } from "../store";
import { useUser, type User } from "@tensamin/user/context";
import { useUserFields } from "@tensamin/user/context";
const USER_FIELDS = ["Display"] as const;
function Participant({ userId }: { userId: number }) {
const { data: user } = useUserFields(userId, USER_FIELDS);
return user ? <p className="text-2xl">User: {user.Display}</p> : null;
}
export default function Preview() {
const { get } = useUser();
const currentCallData = useCall((state) => state.currentCallData);
const [data, setData] = useState<User[]>([]);
useEffect(() => {
let active = true;
if (!currentCallData?.exists) {
return () => {
active = false;
};
}
void Promise.all(currentCallData.UserIds.map((id) => get(id)))
.then((users) => {
if (!active) {
return;
}
setData(users);
})
.catch(() => {
if (!active) {
return;
}
setData([]);
});
return () => {
active = false;
};
}, [currentCallData, get]);
return (
<div className="w-full h-full flex items-center justify-center">
{currentCallData?.exists ? (
<div className="flex flex-col gap-2">
{data.map((user) => {
return (
<p key={user.UserId} className="text-2xl">
User: {user.Display}
</p>
);
})}
{currentCallData.UserIds.map((userId) => (
<Participant key={userId} userId={userId} />
))}
</div>
) : (
<p className="text-2xl">Call expired</p>