feat(status): make the set-status menu work
feat(user): optimise get() function for react qol(todos): update todos
This commit is contained in:
parent
dbb407606b
commit
1c90a0c859
30 changed files with 572 additions and 324 deletions
|
|
@ -63,6 +63,7 @@ export default function InviteButton({
|
|||
<Wrapper
|
||||
key={contact.UserId}
|
||||
userId={contact.UserId}
|
||||
fields={["Display"]}
|
||||
loading={<div>Loading...</div>}
|
||||
component={(user) => (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default function InvitePopup({
|
|||
return (
|
||||
<Wrapper
|
||||
userId={user}
|
||||
fields={["Avatar", "Display"]}
|
||||
loading={null}
|
||||
component={(user) => (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue