(feat): add reply box to messages
(feat): update methanium ui (fix): user data caching (fix): other random stuff (qol): update todo
This commit is contained in:
parent
53728b7436
commit
6a5236bafe
32 changed files with 722 additions and 394 deletions
|
|
@ -31,7 +31,7 @@ import {
|
|||
import { useStorage } from "@tensamin/storage/context";
|
||||
|
||||
export default function Actions() {
|
||||
const sharedClasses = "w-14 h-10";
|
||||
const sharedClasses = "w-14! h-10!";
|
||||
const sharedIconSize = 15;
|
||||
const view = useCall((state) => state.view);
|
||||
const focusedParticipantId = useCall((state) => state.focusedParticipantId);
|
||||
|
|
@ -83,13 +83,15 @@ export default function Actions() {
|
|||
{view === "focused" && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
render={({ ref, onClick }) => (
|
||||
<Button
|
||||
onClick={() =>
|
||||
setUsersInFocusedViewHidden(!usersInFocusedViewHidden)
|
||||
}
|
||||
variant="link"
|
||||
className="w-11 h-11 p-0! ml-3 text-foreground"
|
||||
ref={ref as React.Ref<HTMLButtonElement>}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
setUsersInFocusedViewHidden(!usersInFocusedViewHidden);
|
||||
}}
|
||||
variant="ghost"
|
||||
className="w-11 h-11! p-0! ml-3 text-foreground border-0!"
|
||||
>
|
||||
{usersInFocusedViewHidden ? (
|
||||
<ChevronUp className="size-6" />
|
||||
|
|
@ -97,7 +99,7 @@ export default function Actions() {
|
|||
<ChevronDown className="size-6" />
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<TooltipContent portalProps={{ container: portalContainer }}>
|
||||
Hide users
|
||||
|
|
@ -132,11 +134,15 @@ export default function Actions() {
|
|||
{isWatchingFocusedStream && focusedParticipantId !== ownId ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
render={({ ref, onClick }) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
className={sharedClasses}
|
||||
variant="destructive"
|
||||
onClick={() => stopWatchingFocusedStream()}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
stopWatchingFocusedStream();
|
||||
}}
|
||||
>
|
||||
<ScreenShareOff
|
||||
style={{
|
||||
|
|
@ -145,7 +151,7 @@ export default function Actions() {
|
|||
}}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<TooltipContent portalProps={{ container: portalContainer }}>
|
||||
Stop watching
|
||||
|
|
@ -161,14 +167,18 @@ export default function Actions() {
|
|||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="w-30 flex justify-end">
|
||||
<div className="w-30 flex justify-end gap-1.5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
render={({ ref, onClick }) => (
|
||||
<Button
|
||||
onClick={() => setCallIsPopout(!callIsPopout)}
|
||||
variant="link"
|
||||
className="w-11 h-11 p-0! text-foreground"
|
||||
ref={ref}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
setCallIsPopout(!callIsPopout);
|
||||
}}
|
||||
variant="ghost"
|
||||
className="w-11 h-11! p-0! text-foreground border-0!"
|
||||
>
|
||||
{callIsPopout ? (
|
||||
<SquareArrowOutDownLeft className="size-6" />
|
||||
|
|
@ -176,7 +186,7 @@ export default function Actions() {
|
|||
<SquareArrowOutUpRight className="size-6" />
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<TooltipContent portalProps={{ container: portalContainer }}>
|
||||
Popout
|
||||
|
|
@ -184,13 +194,15 @@ export default function Actions() {
|
|||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
render={({ ref, onClick }) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
ref={ref}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
void toggleFullscreen();
|
||||
}}
|
||||
variant="link"
|
||||
className="w-11 h-11 p-0! mr-3 text-foreground"
|
||||
variant="ghost"
|
||||
className="w-11 h-11! p-0! mr-3 text-foreground border-0!"
|
||||
>
|
||||
{callIsFullscreen ? (
|
||||
<Minimize className="size-6" />
|
||||
|
|
@ -198,7 +210,7 @@ export default function Actions() {
|
|||
<Maximize className="size-6" />
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<TooltipContent portalProps={{ container: portalContainer }}>
|
||||
Fullscreen
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { Button, withTooltip } from "@methanium/ui";
|
||||
import { Button } from "@methanium/ui";
|
||||
import { toggleDeaf, useCall } from "../../store";
|
||||
import { HeadphoneOff, Headphones } from "lucide-react";
|
||||
import { type CallButtonProps, iconScale } from "./tooltipButton";
|
||||
import {
|
||||
type CallButtonProps,
|
||||
iconScale,
|
||||
withButtonTooltip,
|
||||
} from "./tooltipButton";
|
||||
|
||||
export default function DeafButton({
|
||||
className,
|
||||
|
|
@ -25,5 +29,5 @@ export default function DeafButton({
|
|||
</Button>
|
||||
);
|
||||
|
||||
return withTooltip(button, tooltip, portalContainer);
|
||||
return withButtonTooltip(button, tooltip, portalContainer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,13 +38,13 @@ export default function InviteButton({
|
|||
render={
|
||||
tooltip ? (
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button className={className}>
|
||||
render={({ ref, onClick }) => (
|
||||
<Button ref={ref} onClick={onClick} className={className}>
|
||||
<Mail
|
||||
style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Button className={className}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { LeaveIcon } from "@livekit/components-react";
|
||||
import { Button, withTooltip } from "@methanium/ui";
|
||||
import { Button } from "@methanium/ui";
|
||||
import { disconnect, useCall } from "../../store";
|
||||
import { type CallButtonProps, iconScale } from "./tooltipButton";
|
||||
import {
|
||||
type CallButtonProps,
|
||||
iconScale,
|
||||
withButtonTooltip,
|
||||
} from "./tooltipButton";
|
||||
|
||||
export default function LeaveButton({
|
||||
className,
|
||||
|
|
@ -22,5 +26,5 @@ export default function LeaveButton({
|
|||
</Button>
|
||||
);
|
||||
|
||||
return withTooltip(button, tooltip, portalContainer);
|
||||
return withButtonTooltip(button, tooltip, portalContainer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { Button, withTooltip } from "@methanium/ui";
|
||||
import { Button } from "@methanium/ui";
|
||||
import { toggleMute, useCall } from "../../store";
|
||||
import { Mic, MicOff } from "lucide-react";
|
||||
import { type CallButtonProps, iconScale } from "./tooltipButton";
|
||||
import {
|
||||
type CallButtonProps,
|
||||
iconScale,
|
||||
withButtonTooltip,
|
||||
} from "./tooltipButton";
|
||||
|
||||
export default function MuteButton({
|
||||
className,
|
||||
|
|
@ -25,5 +29,5 @@ export default function MuteButton({
|
|||
</Button>
|
||||
);
|
||||
|
||||
return withTooltip(button, tooltip, portalContainer);
|
||||
return withButtonTooltip(button, tooltip, portalContainer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,11 +69,16 @@ export default function ScreenshareButton({
|
|||
<Tooltip>
|
||||
<Popover onOpenChange={setMenuOpen} open={menuOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
render={({ onClick: popoverClick }) =>
|
||||
tooltip ? (
|
||||
<TooltipTrigger
|
||||
render={
|
||||
render={({ ref, onClick: tooltipClick }) => (
|
||||
<Button
|
||||
ref={ref as React.Ref<HTMLButtonElement>}
|
||||
onClick={(event) => {
|
||||
popoverClick?.(event);
|
||||
tooltipClick?.(event);
|
||||
}}
|
||||
variant={isScreensharing ? "subtleDefault" : "default"}
|
||||
className={className}
|
||||
>
|
||||
|
|
@ -91,12 +96,13 @@ export default function ScreenshareButton({
|
|||
/>
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant={isScreensharing ? "subtleDefault" : "default"}
|
||||
className={className}
|
||||
onClick={popoverClick}
|
||||
>
|
||||
{isScreensharing ? (
|
||||
<MonitorDot
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import { cloneElement } from "react";
|
||||
import type { MouseEvent as ReactMouseEvent, ReactElement, Ref } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@methanium/ui";
|
||||
|
||||
export type CallButtonProps = {
|
||||
className?: string;
|
||||
iconSize?: number;
|
||||
|
|
@ -8,3 +12,33 @@ export type CallButtonProps = {
|
|||
export function iconScale(iconSize?: number) {
|
||||
return { scale: (iconSize ? iconSize + 100 : 100) + "%" };
|
||||
}
|
||||
|
||||
export function withButtonTooltip(
|
||||
trigger: ReactElement<{
|
||||
onClick?: (event: ReactMouseEvent<HTMLElement>) => void;
|
||||
ref?: Ref<HTMLElement>;
|
||||
}>,
|
||||
content?: string,
|
||||
portalContainer?: HTMLElement,
|
||||
) {
|
||||
if (!content) return trigger;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={({ ref, onClick }) =>
|
||||
cloneElement(trigger, {
|
||||
ref,
|
||||
onClick: (event) => {
|
||||
onClick?.(event);
|
||||
trigger.props.onClick?.(event);
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<TooltipContent portalProps={{ container: portalContainer }}>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -264,9 +264,15 @@ export default function Base({
|
|||
render={
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`relative w-full ${fill ? "h-full border-0" : "aspect-video border-2"} ${
|
||||
className={`relative w-full ${fill ? "h-full" : "aspect-video"} ${
|
||||
flush || fill ? "rounded-none" : "rounded-md"
|
||||
} ${flush && !fill ? "border-x-0" : ""}`}
|
||||
} ${
|
||||
type === "user" && isSpeaking
|
||||
? "border-4 border-(--primary-foreground-alt)/75"
|
||||
: fill
|
||||
? "border-0"
|
||||
: "border"
|
||||
} ${flush && !fill ? "border-x-0!" : ""}`}
|
||||
>
|
||||
<div className="z-20 absolute bottom-0 left-0 w-full h-full flex justify-start items-end p-2">
|
||||
<>
|
||||
|
|
@ -304,7 +310,7 @@ export default function Base({
|
|||
ref={currentCard}
|
||||
className={`transition-all duration-150 z-10 bg-card absolute top-0 left-0 w-full h-full flex gap-2 items-center justify-center ${
|
||||
flush ? "rounded-none" : "rounded-sm"
|
||||
} ${type === "user" && isSpeaking ? "border-4 border-(--primary-foreground-alt)/75" : "border-0"}`}
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: avatarBackgroundColor,
|
||||
containerType: "size",
|
||||
|
|
|
|||
|
|
@ -18,25 +18,36 @@ import { useEffect, useState } from "react";
|
|||
import { AreaChart, Area } from "recharts";
|
||||
import LeaveButton from "./buttons/leave";
|
||||
|
||||
export default function SidebarBox() {
|
||||
const state = useCall((store) => store.state);
|
||||
const isMobile = useIsMobile();
|
||||
async function getPing(room: Room): Promise<number | undefined> {
|
||||
const report = await room.localParticipant
|
||||
.getTrackPublication(Track.Source.Microphone)
|
||||
?.track?.getRTCStatsReport();
|
||||
|
||||
return state === "closed" ? null : (
|
||||
<Card className="p-1.5 gap-2" hidden={isMobile}>
|
||||
<CardHeader className="p-0! pb-2! border-b-2">
|
||||
<ConnectionBar />
|
||||
</CardHeader>
|
||||
<CardContent className="p-0! flex flex-col gap-1">
|
||||
<div className="flex justify-start gap-1">
|
||||
<MuteButton className="w-9 h-9" />
|
||||
<DeafButton className="w-9 h-9" />
|
||||
<ScreenshareButton className="w-9 h-9" defaultPortal />
|
||||
<LeaveButton className="w-9 h-9" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
if (!report) return;
|
||||
|
||||
let bestRtt: number | undefined;
|
||||
|
||||
report.forEach((stat) => {
|
||||
if (
|
||||
stat.type === "candidate-pair" &&
|
||||
stat.state === "succeeded" &&
|
||||
stat.currentRoundTripTime != null
|
||||
) {
|
||||
bestRtt = stat.currentRoundTripTime * 1000;
|
||||
}
|
||||
|
||||
if (stat.type === "remote-inbound-rtp" && stat.roundTripTime != null) {
|
||||
bestRtt = stat.roundTripTime * 1000;
|
||||
}
|
||||
});
|
||||
|
||||
if (bestRtt == null || bestRtt <= 0) return;
|
||||
|
||||
const roundedRtt = Math.round(bestRtt);
|
||||
|
||||
if (roundedRtt <= 0) return;
|
||||
|
||||
return roundedRtt;
|
||||
}
|
||||
|
||||
function ConnectionBar() {
|
||||
|
|
@ -47,14 +58,18 @@ function ConnectionBar() {
|
|||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
render={({ ref, onClick }) => (
|
||||
<Button
|
||||
onClick={() => openCallPage(callId || "")}
|
||||
ref={ref as React.Ref<HTMLButtonElement>}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
openCallPage(callId || "");
|
||||
}}
|
||||
size="lg"
|
||||
variant={
|
||||
state === "open" && isEncrypted ? "subtleDefault" : "destructive"
|
||||
}
|
||||
className="flex justify-between items-center"
|
||||
className="flex items-center justify-between px-2!"
|
||||
>
|
||||
{state === "encrypting" && "Encrypting..."}
|
||||
{state === "connecting" && "Connecting..."}
|
||||
|
|
@ -70,7 +85,7 @@ function ConnectionBar() {
|
|||
<LockOpen color="var(--destructive)" />
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>Click to open call page</TooltipContent>
|
||||
</Tooltip>
|
||||
|
|
@ -171,34 +186,23 @@ export function TinyPingGraph() {
|
|||
);
|
||||
}
|
||||
|
||||
async function getPing(room: Room): Promise<number | undefined> {
|
||||
const report = await room.localParticipant
|
||||
.getTrackPublication(Track.Source.Microphone)
|
||||
?.track?.getRTCStatsReport();
|
||||
export default function SidebarBox() {
|
||||
const state = useCall((store) => store.state);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (!report) return;
|
||||
|
||||
let bestRtt: number | undefined;
|
||||
|
||||
report.forEach((stat) => {
|
||||
if (
|
||||
stat.type === "candidate-pair" &&
|
||||
stat.state === "succeeded" &&
|
||||
stat.currentRoundTripTime != null
|
||||
) {
|
||||
bestRtt = stat.currentRoundTripTime * 1000;
|
||||
}
|
||||
|
||||
if (stat.type === "remote-inbound-rtp" && stat.roundTripTime != null) {
|
||||
bestRtt = stat.roundTripTime * 1000;
|
||||
}
|
||||
});
|
||||
|
||||
if (bestRtt == null || bestRtt <= 0) return;
|
||||
|
||||
const roundedRtt = Math.round(bestRtt);
|
||||
|
||||
if (roundedRtt <= 0) return;
|
||||
|
||||
return roundedRtt;
|
||||
return state === "closed" ? null : (
|
||||
<Card className="p-1.5 gap-2" hidden={isMobile}>
|
||||
<CardHeader className="p-0! pb-2! border-b-2">
|
||||
<ConnectionBar />
|
||||
</CardHeader>
|
||||
<CardContent className="p-0! flex flex-col gap-1">
|
||||
<div className="flex justify-center gap-1">
|
||||
<MuteButton className="h-9! w-[24%]" />
|
||||
<DeafButton className="h-9! w-[24%]" />
|
||||
<ScreenshareButton className="h-9! w-[24%]" defaultPortal />
|
||||
<LeaveButton className="h-9! w-[24%]" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue