(feat): big call and chatting stuff #23
32 changed files with 722 additions and 394 deletions
(feat): add reply box to messages
(feat): update methanium ui (fix): user data caching (fix): other random stuff (qol): update todo
commit
6a5236bafe
|
|
@ -29,7 +29,7 @@ export function Basic({
|
||||||
render={<div />}
|
render={<div />}
|
||||||
nativeButton={false}
|
nativeButton={false}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="animate-in fade-in duration-300 h-auto w-full justify-start gap-2.5 rounded-xl min-h-12.5!"
|
className="outline-none! animate-in fade-in duration-300 h-auto w-full justify-start gap-2.5 rounded-xl min-h-12.5!"
|
||||||
>
|
>
|
||||||
<div className="relative shrink-0 overflow-visible">
|
<div className="relative shrink-0 overflow-visible">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export default function Profile({ user }: { user: User }) {
|
||||||
<Text value={user.About || ""} />
|
<Text value={user.About || ""} />
|
||||||
<Button
|
<Button
|
||||||
className="h-auto justify-start gap-1.5 px-0 py-1 text-base font-medium text-white no-underline hover:no-underline"
|
className="h-auto justify-start gap-1.5 px-0 py-1 text-base font-medium text-white no-underline hover:no-underline"
|
||||||
variant="link"
|
variant="ghost"
|
||||||
aria-expanded={showAdvancedInformation}
|
aria-expanded={showAdvancedInformation}
|
||||||
onClick={() => setShowAdvancedInformation((show) => !show)}
|
onClick={() => setShowAdvancedInformation((show) => !show)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -43,16 +43,21 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className={`${forMobile && "border-b"} w-full shrink-0 gap-2 h-13.5 flex items-center justify-between`}
|
className={`${forMobile && "border-b"} pl-px w-full shrink-0 gap-2 h-13.5 flex items-center justify-between`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
{forMobile ? (
|
{forMobile ? (
|
||||||
<SidebarTrigger
|
<SidebarTrigger
|
||||||
className="w-9 h-9! aspect-square rounded-lg ml-2"
|
render={({ onClick }) => (
|
||||||
variant="outline"
|
<Button
|
||||||
>
|
onClick={onClick}
|
||||||
<ArrowLeft className="size-4.5" />
|
className="w-9 h-9! ml-2"
|
||||||
</SidebarTrigger>
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="size-4.5" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -80,10 +85,11 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
) : (
|
) : (
|
||||||
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
|
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
render={
|
render={({ onClick }) => (
|
||||||
<Button
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
variant="link"
|
variant="link"
|
||||||
className="px-0! text-foreground"
|
className="text-foreground px-0! border-0! bg-none! bg-transparent!"
|
||||||
style={{
|
style={{
|
||||||
textDecorationLine: "none",
|
textDecorationLine: "none",
|
||||||
}}
|
}}
|
||||||
|
|
@ -92,7 +98,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
{user?.Display}
|
{user?.Display}
|
||||||
</p>
|
</p>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
<PopoverContent side="bottom">
|
<PopoverContent side="bottom">
|
||||||
<Profile user={user} />
|
<Profile user={user} />
|
||||||
|
|
@ -191,7 +197,7 @@ export function MobileNavbar() {
|
||||||
navigate({ to: "/" });
|
navigate({ to: "/" });
|
||||||
setOpenMobile(false);
|
setOpenMobile(false);
|
||||||
}}
|
}}
|
||||||
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1 border-0! hover:bg-transparent!"
|
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1 border-0! bg-none! bg-transparent!"
|
||||||
variant="link"
|
variant="link"
|
||||||
>
|
>
|
||||||
<House className="size-4.5" />
|
<House className="size-4.5" />
|
||||||
|
|
@ -202,7 +208,7 @@ export function MobileNavbar() {
|
||||||
navigate({ to: "/settings" });
|
navigate({ to: "/settings" });
|
||||||
setOpenMobile(false);
|
setOpenMobile(false);
|
||||||
}}
|
}}
|
||||||
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1 border-0! hover:bg-transparent!"
|
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1 border-0! bg-none! bg-transparent!"
|
||||||
variant="link"
|
variant="link"
|
||||||
>
|
>
|
||||||
<Settings className="size-4.5" />
|
<Settings className="size-4.5" />
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ export default function Sidebar() {
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
data-slot="sidebar"
|
data-slot="sidebar"
|
||||||
data-mobile="true"
|
data-mobile="true"
|
||||||
className="fixed inset-y-0 left-0 z-50 w-screen bg-sidebar p-0 text-sidebar-foreground transition-[transform,opacity] duration-150 ease-linear"
|
className="fixed inset-y-0 left-0 z-50 w-screen bg-sidebar p-0 text-sidebar-foreground"
|
||||||
style={{
|
style={{
|
||||||
transform: openMobile ? "translateX(0)" : "translateX(-100%)",
|
transform: openMobile ? "translateX(0)" : "translateX(-100%)",
|
||||||
opacity: openMobile ? 1 : 0,
|
opacity: openMobile ? 1 : 0,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export default function List() {
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
id="conversation-list"
|
id="conversation-list"
|
||||||
className="overflow-y-auto flex-1 h-full"
|
className="overflow-y-auto flex-1 h-full p-px"
|
||||||
>
|
>
|
||||||
{items === null ? (
|
{items === null ? (
|
||||||
<div className="flex items-center justify-center pt-5">
|
<div className="flex items-center justify-center pt-5">
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ function AddConversationButton() {
|
||||||
<Button onClick={onClick}>Add Conversation</Button>
|
<Button onClick={onClick}>Add Conversation</Button>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogContent>
|
<DialogContent showCloseButton={false}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>New Conversation</DialogTitle>
|
<DialogTitle>New Conversation</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import {
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
|
|
||||||
export default function Actions() {
|
export default function Actions() {
|
||||||
const sharedClasses = "w-14 h-10";
|
const sharedClasses = "w-14! h-10!";
|
||||||
const sharedIconSize = 15;
|
const sharedIconSize = 15;
|
||||||
const view = useCall((state) => state.view);
|
const view = useCall((state) => state.view);
|
||||||
const focusedParticipantId = useCall((state) => state.focusedParticipantId);
|
const focusedParticipantId = useCall((state) => state.focusedParticipantId);
|
||||||
|
|
@ -83,13 +83,15 @@ export default function Actions() {
|
||||||
{view === "focused" && (
|
{view === "focused" && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={({ ref, onClick }) => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
ref={ref as React.Ref<HTMLButtonElement>}
|
||||||
setUsersInFocusedViewHidden(!usersInFocusedViewHidden)
|
onClick={(event) => {
|
||||||
}
|
onClick?.(event);
|
||||||
variant="link"
|
setUsersInFocusedViewHidden(!usersInFocusedViewHidden);
|
||||||
className="w-11 h-11 p-0! ml-3 text-foreground"
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
className="w-11 h-11! p-0! ml-3 text-foreground border-0!"
|
||||||
>
|
>
|
||||||
{usersInFocusedViewHidden ? (
|
{usersInFocusedViewHidden ? (
|
||||||
<ChevronUp className="size-6" />
|
<ChevronUp className="size-6" />
|
||||||
|
|
@ -97,7 +99,7 @@ export default function Actions() {
|
||||||
<ChevronDown className="size-6" />
|
<ChevronDown className="size-6" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
<TooltipContent portalProps={{ container: portalContainer }}>
|
<TooltipContent portalProps={{ container: portalContainer }}>
|
||||||
Hide users
|
Hide users
|
||||||
|
|
@ -132,11 +134,15 @@ export default function Actions() {
|
||||||
{isWatchingFocusedStream && focusedParticipantId !== ownId ? (
|
{isWatchingFocusedStream && focusedParticipantId !== ownId ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={({ ref, onClick }) => (
|
||||||
<Button
|
<Button
|
||||||
|
ref={ref}
|
||||||
className={sharedClasses}
|
className={sharedClasses}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => stopWatchingFocusedStream()}
|
onClick={(event) => {
|
||||||
|
onClick?.(event);
|
||||||
|
stopWatchingFocusedStream();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ScreenShareOff
|
<ScreenShareOff
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -145,7 +151,7 @@ export default function Actions() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
<TooltipContent portalProps={{ container: portalContainer }}>
|
<TooltipContent portalProps={{ container: portalContainer }}>
|
||||||
Stop watching
|
Stop watching
|
||||||
|
|
@ -161,14 +167,18 @@ export default function Actions() {
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="w-30 flex justify-end">
|
<div className="w-30 flex justify-end gap-1.5">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={({ ref, onClick }) => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setCallIsPopout(!callIsPopout)}
|
ref={ref}
|
||||||
variant="link"
|
onClick={(event) => {
|
||||||
className="w-11 h-11 p-0! text-foreground"
|
onClick?.(event);
|
||||||
|
setCallIsPopout(!callIsPopout);
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
className="w-11 h-11! p-0! text-foreground border-0!"
|
||||||
>
|
>
|
||||||
{callIsPopout ? (
|
{callIsPopout ? (
|
||||||
<SquareArrowOutDownLeft className="size-6" />
|
<SquareArrowOutDownLeft className="size-6" />
|
||||||
|
|
@ -176,7 +186,7 @@ export default function Actions() {
|
||||||
<SquareArrowOutUpRight className="size-6" />
|
<SquareArrowOutUpRight className="size-6" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
<TooltipContent portalProps={{ container: portalContainer }}>
|
<TooltipContent portalProps={{ container: portalContainer }}>
|
||||||
Popout
|
Popout
|
||||||
|
|
@ -184,13 +194,15 @@ export default function Actions() {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={({ ref, onClick }) => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
ref={ref}
|
||||||
|
onClick={(event) => {
|
||||||
|
onClick?.(event);
|
||||||
void toggleFullscreen();
|
void toggleFullscreen();
|
||||||
}}
|
}}
|
||||||
variant="link"
|
variant="ghost"
|
||||||
className="w-11 h-11 p-0! mr-3 text-foreground"
|
className="w-11 h-11! p-0! mr-3 text-foreground border-0!"
|
||||||
>
|
>
|
||||||
{callIsFullscreen ? (
|
{callIsFullscreen ? (
|
||||||
<Minimize className="size-6" />
|
<Minimize className="size-6" />
|
||||||
|
|
@ -198,7 +210,7 @@ export default function Actions() {
|
||||||
<Maximize className="size-6" />
|
<Maximize className="size-6" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
<TooltipContent portalProps={{ container: portalContainer }}>
|
<TooltipContent portalProps={{ container: portalContainer }}>
|
||||||
Fullscreen
|
Fullscreen
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { Button, withTooltip } from "@methanium/ui";
|
import { Button } from "@methanium/ui";
|
||||||
import { toggleDeaf, useCall } from "../../store";
|
import { toggleDeaf, useCall } from "../../store";
|
||||||
import { HeadphoneOff, Headphones } from "lucide-react";
|
import { HeadphoneOff, Headphones } from "lucide-react";
|
||||||
import { type CallButtonProps, iconScale } from "./tooltipButton";
|
import {
|
||||||
|
type CallButtonProps,
|
||||||
|
iconScale,
|
||||||
|
withButtonTooltip,
|
||||||
|
} from "./tooltipButton";
|
||||||
|
|
||||||
export default function DeafButton({
|
export default function DeafButton({
|
||||||
className,
|
className,
|
||||||
|
|
@ -25,5 +29,5 @@ export default function DeafButton({
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
return withTooltip(button, tooltip, portalContainer);
|
return withButtonTooltip(button, tooltip, portalContainer);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,13 @@ export default function InviteButton({
|
||||||
render={
|
render={
|
||||||
tooltip ? (
|
tooltip ? (
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={({ ref, onClick }) => (
|
||||||
<Button className={className}>
|
<Button ref={ref} onClick={onClick} className={className}>
|
||||||
<Mail
|
<Mail
|
||||||
style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }}
|
style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button className={className}>
|
<Button className={className}>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { LeaveIcon } from "@livekit/components-react";
|
import { LeaveIcon } from "@livekit/components-react";
|
||||||
import { Button, withTooltip } from "@methanium/ui";
|
import { Button } from "@methanium/ui";
|
||||||
import { disconnect, useCall } from "../../store";
|
import { disconnect, useCall } from "../../store";
|
||||||
import { type CallButtonProps, iconScale } from "./tooltipButton";
|
import {
|
||||||
|
type CallButtonProps,
|
||||||
|
iconScale,
|
||||||
|
withButtonTooltip,
|
||||||
|
} from "./tooltipButton";
|
||||||
|
|
||||||
export default function LeaveButton({
|
export default function LeaveButton({
|
||||||
className,
|
className,
|
||||||
|
|
@ -22,5 +26,5 @@ export default function LeaveButton({
|
||||||
</Button>
|
</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 { toggleMute, useCall } from "../../store";
|
||||||
import { Mic, MicOff } from "lucide-react";
|
import { Mic, MicOff } from "lucide-react";
|
||||||
import { type CallButtonProps, iconScale } from "./tooltipButton";
|
import {
|
||||||
|
type CallButtonProps,
|
||||||
|
iconScale,
|
||||||
|
withButtonTooltip,
|
||||||
|
} from "./tooltipButton";
|
||||||
|
|
||||||
export default function MuteButton({
|
export default function MuteButton({
|
||||||
className,
|
className,
|
||||||
|
|
@ -25,5 +29,5 @@ export default function MuteButton({
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
return withTooltip(button, tooltip, portalContainer);
|
return withButtonTooltip(button, tooltip, portalContainer);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,16 @@ export default function ScreenshareButton({
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Popover onOpenChange={setMenuOpen} open={menuOpen}>
|
<Popover onOpenChange={setMenuOpen} open={menuOpen}>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
render={
|
render={({ onClick: popoverClick }) =>
|
||||||
tooltip ? (
|
tooltip ? (
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={({ ref, onClick: tooltipClick }) => (
|
||||||
<Button
|
<Button
|
||||||
|
ref={ref as React.Ref<HTMLButtonElement>}
|
||||||
|
onClick={(event) => {
|
||||||
|
popoverClick?.(event);
|
||||||
|
tooltipClick?.(event);
|
||||||
|
}}
|
||||||
variant={isScreensharing ? "subtleDefault" : "default"}
|
variant={isScreensharing ? "subtleDefault" : "default"}
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
|
|
@ -91,12 +96,13 @@ export default function ScreenshareButton({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant={isScreensharing ? "subtleDefault" : "default"}
|
variant={isScreensharing ? "subtleDefault" : "default"}
|
||||||
className={className}
|
className={className}
|
||||||
|
onClick={popoverClick}
|
||||||
>
|
>
|
||||||
{isScreensharing ? (
|
{isScreensharing ? (
|
||||||
<MonitorDot
|
<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 = {
|
export type CallButtonProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
iconSize?: number;
|
iconSize?: number;
|
||||||
|
|
@ -8,3 +12,33 @@ export type CallButtonProps = {
|
||||||
export function iconScale(iconSize?: number) {
|
export function iconScale(iconSize?: number) {
|
||||||
return { scale: (iconSize ? iconSize + 100 : 100) + "%" };
|
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={
|
render={
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
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 ? "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">
|
<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}
|
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 ${
|
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"
|
flush ? "rounded-none" : "rounded-sm"
|
||||||
} ${type === "user" && isSpeaking ? "border-4 border-(--primary-foreground-alt)/75" : "border-0"}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: avatarBackgroundColor,
|
backgroundColor: avatarBackgroundColor,
|
||||||
containerType: "size",
|
containerType: "size",
|
||||||
|
|
|
||||||
|
|
@ -18,25 +18,36 @@ import { useEffect, useState } from "react";
|
||||||
import { AreaChart, Area } from "recharts";
|
import { AreaChart, Area } from "recharts";
|
||||||
import LeaveButton from "./buttons/leave";
|
import LeaveButton from "./buttons/leave";
|
||||||
|
|
||||||
export default function SidebarBox() {
|
async function getPing(room: Room): Promise<number | undefined> {
|
||||||
const state = useCall((store) => store.state);
|
const report = await room.localParticipant
|
||||||
const isMobile = useIsMobile();
|
.getTrackPublication(Track.Source.Microphone)
|
||||||
|
?.track?.getRTCStatsReport();
|
||||||
|
|
||||||
return state === "closed" ? null : (
|
if (!report) return;
|
||||||
<Card className="p-1.5 gap-2" hidden={isMobile}>
|
|
||||||
<CardHeader className="p-0! pb-2! border-b-2">
|
let bestRtt: number | undefined;
|
||||||
<ConnectionBar />
|
|
||||||
</CardHeader>
|
report.forEach((stat) => {
|
||||||
<CardContent className="p-0! flex flex-col gap-1">
|
if (
|
||||||
<div className="flex justify-start gap-1">
|
stat.type === "candidate-pair" &&
|
||||||
<MuteButton className="w-9 h-9" />
|
stat.state === "succeeded" &&
|
||||||
<DeafButton className="w-9 h-9" />
|
stat.currentRoundTripTime != null
|
||||||
<ScreenshareButton className="w-9 h-9" defaultPortal />
|
) {
|
||||||
<LeaveButton className="w-9 h-9" />
|
bestRtt = stat.currentRoundTripTime * 1000;
|
||||||
</div>
|
}
|
||||||
</CardContent>
|
|
||||||
</Card>
|
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() {
|
function ConnectionBar() {
|
||||||
|
|
@ -47,14 +58,18 @@ function ConnectionBar() {
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={({ ref, onClick }) => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => openCallPage(callId || "")}
|
ref={ref as React.Ref<HTMLButtonElement>}
|
||||||
|
onClick={(event) => {
|
||||||
|
onClick?.(event);
|
||||||
|
openCallPage(callId || "");
|
||||||
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant={
|
variant={
|
||||||
state === "open" && isEncrypted ? "subtleDefault" : "destructive"
|
state === "open" && isEncrypted ? "subtleDefault" : "destructive"
|
||||||
}
|
}
|
||||||
className="flex justify-between items-center"
|
className="flex items-center justify-between px-2!"
|
||||||
>
|
>
|
||||||
{state === "encrypting" && "Encrypting..."}
|
{state === "encrypting" && "Encrypting..."}
|
||||||
{state === "connecting" && "Connecting..."}
|
{state === "connecting" && "Connecting..."}
|
||||||
|
|
@ -70,7 +85,7 @@ function ConnectionBar() {
|
||||||
<LockOpen color="var(--destructive)" />
|
<LockOpen color="var(--destructive)" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
<TooltipContent>Click to open call page</TooltipContent>
|
<TooltipContent>Click to open call page</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -171,34 +186,23 @@ export function TinyPingGraph() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPing(room: Room): Promise<number | undefined> {
|
export default function SidebarBox() {
|
||||||
const report = await room.localParticipant
|
const state = useCall((store) => store.state);
|
||||||
.getTrackPublication(Track.Source.Microphone)
|
const isMobile = useIsMobile();
|
||||||
?.track?.getRTCStatsReport();
|
|
||||||
|
|
||||||
if (!report) return;
|
return state === "closed" ? null : (
|
||||||
|
<Card className="p-1.5 gap-2" hidden={isMobile}>
|
||||||
let bestRtt: number | undefined;
|
<CardHeader className="p-0! pb-2! border-b-2">
|
||||||
|
<ConnectionBar />
|
||||||
report.forEach((stat) => {
|
</CardHeader>
|
||||||
if (
|
<CardContent className="p-0! flex flex-col gap-1">
|
||||||
stat.type === "candidate-pair" &&
|
<div className="flex justify-center gap-1">
|
||||||
stat.state === "succeeded" &&
|
<MuteButton className="h-9! w-[24%]" />
|
||||||
stat.currentRoundTripTime != null
|
<DeafButton className="h-9! w-[24%]" />
|
||||||
) {
|
<ScreenshareButton className="h-9! w-[24%]" defaultPortal />
|
||||||
bestRtt = stat.currentRoundTripTime * 1000;
|
<LeaveButton className="h-9! w-[24%]" />
|
||||||
}
|
</div>
|
||||||
|
</CardContent>
|
||||||
if (stat.type === "remote-inbound-rtp" && stat.roundTripTime != null) {
|
</Card>
|
||||||
bestRtt = stat.roundTripTime * 1000;
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bestRtt == null || bestRtt <= 0) return;
|
|
||||||
|
|
||||||
const roundedRtt = Math.round(bestRtt);
|
|
||||||
|
|
||||||
if (roundedRtt <= 0) return;
|
|
||||||
|
|
||||||
return roundedRtt;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1585,7 +1585,7 @@ export function useInitializeCall() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentCallData({
|
setCurrentCallData({
|
||||||
...(data.data as z.infer<typeof mtp.CallData.response>),
|
...mtp.CallData.response.parse(data.data),
|
||||||
exists: true,
|
exists: true,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
import Input from "@tensamin/markdown/input";
|
import Input from "@tensamin/markdown/input";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
Skeleton,
|
|
||||||
} from "@methanium/ui";
|
} from "@methanium/ui";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
import { Button } from "@methanium/ui";
|
import { Button } from "@methanium/ui";
|
||||||
|
|
||||||
import { Plus, Laugh, FileVideo, Forward, X } from "lucide-react";
|
import { Plus, Laugh, FileVideo } from "lucide-react";
|
||||||
import { useChat, useReplyMessage } from "../context";
|
import { useChat, useReplyMessage } from "../context";
|
||||||
import { useMTP } from "@tensamin/mtp";
|
import { useMTP } from "@tensamin/mtp";
|
||||||
import { log, toast } from "@tensamin/shared/log";
|
import { log, toast } from "@tensamin/shared/log";
|
||||||
|
|
@ -25,8 +21,7 @@ import { useSession } from "@tensamin/storage/session";
|
||||||
import GifPicker from "./gifPicker";
|
import GifPicker from "./gifPicker";
|
||||||
import EmojiPicker from "./emojiPicker";
|
import EmojiPicker from "./emojiPicker";
|
||||||
import { useEmojiRanks, useRecordEmojiUse } from "./emojiRanks";
|
import { useEmojiRanks, useRecordEmojiUse } from "./emojiRanks";
|
||||||
import Wrapper from "@tensamin/user/wrapper";
|
import ReplyBox from "./replyBox";
|
||||||
import Text from "@tensamin/markdown/text";
|
|
||||||
|
|
||||||
export default function InputComponent({
|
export default function InputComponent({
|
||||||
value,
|
value,
|
||||||
|
|
@ -196,54 +191,15 @@ export default function InputComponent({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
{/* reply */}
|
||||||
{replyTo !== undefined && (
|
{replyTo !== undefined && (
|
||||||
<div className="self-center rounded-t-lg bg-background border-t border-x flex items-center justify-start w-[90%] p-1">
|
<ReplyBox
|
||||||
<div className="flex gap-1 w-full items-center">
|
content={replyMessage?.Content}
|
||||||
<Forward
|
loading={!replyMessage}
|
||||||
color="var(--muted-foreground)"
|
onDismiss={() => setReplyTo(undefined)}
|
||||||
className="opacity-60"
|
userId={replyUserId ?? ownId}
|
||||||
size={18}
|
variant="composer"
|
||||||
/>
|
/>
|
||||||
{replyMessage ? (
|
|
||||||
<>
|
|
||||||
<Wrapper
|
|
||||||
userId={replyUserId ?? ownId}
|
|
||||||
loading={<Skeleton className="rounded-full" />}
|
|
||||||
component={(user) => (
|
|
||||||
<Avatar>
|
|
||||||
<AvatarImage src={user.Avatar} />
|
|
||||||
<AvatarFallback>
|
|
||||||
{user.Display.slice(2, 0).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="max-w-full max-h-5 h-5 text-xs truncate">
|
|
||||||
<Text value={replyMessage.Content} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Skeleton className="w-5 h-5 aspect-square rounded-full" />
|
|
||||||
<Skeleton className="w-full h-5" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setReplyTo(undefined);
|
|
||||||
}}
|
|
||||||
className="w-5 h-5 rounded-md! p-0!"
|
|
||||||
size="xs"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<X
|
|
||||||
color="var(--muted-foreground)"
|
|
||||||
className="opacity-60"
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<Card
|
<Card
|
||||||
ref={inputBoxRef}
|
ref={inputBoxRef}
|
||||||
|
|
@ -267,7 +223,7 @@ export default function InputComponent({
|
||||||
/>
|
/>
|
||||||
<div className="w-full flex justify-between gap-1 p-1 pt-0">
|
<div className="w-full flex justify-between gap-1 p-1 pt-0">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button className="w-9 h-9 p-0" variant="ghost">
|
<Button className="w-9 h-9! p-0" variant="ghost">
|
||||||
<Plus size={20} />
|
<Plus size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="w-auto flex text-red-500">
|
<div className="w-auto flex text-red-500">
|
||||||
|
|
@ -280,15 +236,16 @@ export default function InputComponent({
|
||||||
onOpenChange={setEmojiPopoverOpen}
|
onOpenChange={setEmojiPopoverOpen}
|
||||||
>
|
>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
render={
|
render={({ onClick }) => (
|
||||||
<Button
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
aria-label="Open emoji picker"
|
aria-label="Open emoji picker"
|
||||||
className="w-9 h-9 p-0"
|
className="w-9 h-9! p-0"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<Laugh size={20} />
|
<Laugh size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
<PopoverContent className="w-auto p-0">
|
<PopoverContent className="w-auto p-0">
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
|
|
@ -302,11 +259,15 @@ export default function InputComponent({
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover open={gifPopoverOpen} onOpenChange={setGifPopoverOpen}>
|
<Popover open={gifPopoverOpen} onOpenChange={setGifPopoverOpen}>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
render={
|
render={({ onClick }) => (
|
||||||
<Button className="w-9 h-9 p-0" variant="ghost">
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
className="w-9 h-9! p-0"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
<FileVideo size={20} />
|
<FileVideo size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
ref={gifPopoverRef}
|
ref={gifPopoverRef}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,13 @@ import Media from "./media";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { useMTP } from "@tensamin/mtp";
|
import { useMTP } from "@tensamin/mtp";
|
||||||
import Input from "@tensamin/markdown/input";
|
import Input from "@tensamin/markdown/input";
|
||||||
import { useChat } from "../context";
|
import { getMessage, useChat } from "../context";
|
||||||
import { encryptChatText } from "@tensamin/crypto/chatSecret";
|
import { decryptChatText, encryptChatText } from "@tensamin/crypto/chatSecret";
|
||||||
import { log, toast } from "@tensamin/shared/log";
|
import { log, toast } from "@tensamin/shared/log";
|
||||||
import Emoji, { normalizeShortcode } from "@tensamin/markdown/emoji";
|
import Emoji, { normalizeShortcode } from "@tensamin/markdown/emoji";
|
||||||
import { useRecordEmojiUse } from "./emojiRanks";
|
import { useRecordEmojiUse } from "./emojiRanks";
|
||||||
|
import { useUser } from "@tensamin/user/context";
|
||||||
|
import ReplyBox from "./replyBox";
|
||||||
|
|
||||||
function MessageComponent({
|
function MessageComponent({
|
||||||
grouped,
|
grouped,
|
||||||
|
|
@ -141,7 +143,45 @@ function MessageComponent({
|
||||||
userId,
|
userId,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
removeReaction,
|
removeReaction,
|
||||||
|
replyTo,
|
||||||
} = useChat();
|
} = useChat();
|
||||||
|
const { get: getUser } = useUser();
|
||||||
|
const [replyMessage, setReplyMessage] = useState<RawMessage | null>(null);
|
||||||
|
const [replyUser, setReplyUser] = useState<User | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!message.ReplyId || !ownId || !chatSecret) {
|
||||||
|
setReplyMessage(null);
|
||||||
|
setReplyUser(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let active = true;
|
||||||
|
void getMessage({
|
||||||
|
sendTime: message.ReplyId,
|
||||||
|
ownId,
|
||||||
|
chatPartnerId: userId,
|
||||||
|
send,
|
||||||
|
})
|
||||||
|
.then(async (reply) => {
|
||||||
|
const [Content, author] = await Promise.all([
|
||||||
|
decryptChatText(chatSecret, reply.Content),
|
||||||
|
getUser(reply.SenderId),
|
||||||
|
]);
|
||||||
|
if (!active) return;
|
||||||
|
setReplyMessage({ ...reply, Content });
|
||||||
|
setReplyUser(author);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!active) return;
|
||||||
|
setReplyMessage(null);
|
||||||
|
setReplyUser(null);
|
||||||
|
log(1, "chat", "red", "Failed to get replied-to message", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [chatSecret, getUser, message.ReplyId, ownId, send, userId]);
|
||||||
const recordUse = useRecordEmojiUse();
|
const recordUse = useRecordEmojiUse();
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [editDraft, setEditDraft] = useState(message.Content);
|
const [editDraft, setEditDraft] = useState(message.Content);
|
||||||
|
|
@ -232,8 +272,16 @@ function MessageComponent({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
// pt-3 is to get a gap between messages
|
// pt-3 is to get a gap between messages
|
||||||
className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${opacityClass}`}
|
className={`${grouped ? "" : "pt-3"} w-full flex flex-col gap-1 justify-start items-start transition-opacity duration-150 ${opacityClass}`}
|
||||||
>
|
>
|
||||||
|
{/* reply */}
|
||||||
|
{replyMessage && replyUser && (
|
||||||
|
<ReplyBox
|
||||||
|
content={replyMessage.Content}
|
||||||
|
user={replyUser}
|
||||||
|
variant="message"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{user && message.Content ? (
|
{user && message.Content ? (
|
||||||
<MessageContextMenu
|
<MessageContextMenu
|
||||||
content={message.Content}
|
content={message.Content}
|
||||||
|
|
@ -243,138 +291,148 @@ function MessageComponent({
|
||||||
onReact={toggleReaction}
|
onReact={toggleReaction}
|
||||||
onSetEditing={setEditing}
|
onSetEditing={setEditing}
|
||||||
>
|
>
|
||||||
<div
|
<>
|
||||||
className={cn(
|
<div
|
||||||
"group hover:bg-muted/50 select-text! relative justify-start flex gap-1 items-center w-full px-2 whitespace-pre-wrap break-all",
|
className={cn(
|
||||||
{
|
"group hover:bg-muted/50 select-text! relative justify-start flex gap-1 items-center w-full px-2 whitespace-pre-wrap break-all transition-colors duration-200 ease-in-out",
|
||||||
"bg-(--destructive)/10 text-destructive-foreground hover:bg-(--destructive)/15":
|
{
|
||||||
actuallyFailed,
|
"bg-(--destructive)/10 text-destructive-foreground hover:bg-(--destructive)/15":
|
||||||
},
|
actuallyFailed,
|
||||||
)}
|
"bg-(--primary-foreground-alt)/15 hover:bg-(--primary-foreground-alt)/20":
|
||||||
>
|
replyTo === message.SendTime && !actuallyFailed,
|
||||||
<>
|
},
|
||||||
{grouped ? (
|
|
||||||
<p className="w-9 text-xs group-hover:visible invisible text-muted-foreground">
|
|
||||||
{new Date(message.SendTime).toLocaleString([], {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<Avatar className="mr-1 mb-auto mt-1 w-10">
|
|
||||||
<AvatarImage src={user.Avatar} />
|
|
||||||
<AvatarFallback>
|
|
||||||
{user.Display.slice(0, 2).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
)}
|
)}
|
||||||
<div
|
>
|
||||||
className={cn("flex flex-col group", {
|
<>
|
||||||
"min-w-0 flex-1": editing,
|
{grouped ? (
|
||||||
})}
|
<p className="w-9 text-xs group-hover:visible invisible text-muted-foreground">
|
||||||
>
|
{new Date(message.SendTime).toLocaleString([], {
|
||||||
{!grouped && (
|
hour: "2-digit",
|
||||||
<div className="flex items-center gap-1">
|
minute: "2-digit",
|
||||||
<p className="font-medium">{user.Display}</p>
|
})}
|
||||||
<p className="text-xs text-muted-foreground">
|
</p>
|
||||||
{new Date(message.SendTime).toLocaleString([], {
|
) : (
|
||||||
hour: "2-digit",
|
<Avatar className="mr-1 mb-auto mt-1 w-10">
|
||||||
minute: "2-digit",
|
<AvatarImage src={user.Avatar} />
|
||||||
})}
|
<AvatarFallback>
|
||||||
</p>
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
<div className="flex items-center justify-start gap-1">
|
</AvatarFallback>
|
||||||
{message.MessageState === "read" ? (
|
</Avatar>
|
||||||
<Check
|
)}
|
||||||
size={12}
|
<div
|
||||||
color="var(--primary-foreground-alt)"
|
className={cn("flex flex-col group", {
|
||||||
/>
|
"min-w-0 flex-1": editing,
|
||||||
) : message.MessageState === "received" ? (
|
})}
|
||||||
<CheckLine size={12} color="var(--muted-foreground)" />
|
>
|
||||||
) : message.MessageState === "sent" ? (
|
{!grouped && (
|
||||||
<Check size={12} color="var(--muted-foreground)" />
|
<div className="flex items-center gap-1">
|
||||||
) : message.MessageState === "sending" ? (
|
<p className="font-semibold">{user.Display}</p>
|
||||||
<RefreshCw size={12} color="var(--muted-foreground)" />
|
<p className="text-xs text-muted-foreground">
|
||||||
) : null}
|
{new Date(message.SendTime).toLocaleString([], {
|
||||||
{actuallyFailed && (
|
hour: "2-digit",
|
||||||
<AlertTriangle color="var(--destructive)" size={12} />
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-start gap-1">
|
||||||
|
{message.MessageState === "read" ? (
|
||||||
|
<Check
|
||||||
|
size={12}
|
||||||
|
color="var(--primary-foreground-alt)"
|
||||||
|
/>
|
||||||
|
) : message.MessageState === "received" ? (
|
||||||
|
<CheckLine
|
||||||
|
size={12}
|
||||||
|
color="var(--muted-foreground)"
|
||||||
|
/>
|
||||||
|
) : message.MessageState === "sent" ? (
|
||||||
|
<Check size={12} color="var(--muted-foreground)" />
|
||||||
|
) : message.MessageState === "sending" ? (
|
||||||
|
<RefreshCw
|
||||||
|
size={12}
|
||||||
|
color="var(--muted-foreground)"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{actuallyFailed && (
|
||||||
|
<AlertTriangle color="var(--destructive)" size={12} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{editing ? (
|
||||||
|
<div className="flex w-full flex-col gap-1">
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
styled
|
||||||
|
setValue={setEditDraft}
|
||||||
|
value={editDraft}
|
||||||
|
onSubmit={() => {
|
||||||
|
submitEditMessage(editDraft);
|
||||||
|
setEditing(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="link"
|
||||||
|
className="text-primary-foreground-alt"
|
||||||
|
onClick={() => {
|
||||||
|
if (editDraft === message.Content) {
|
||||||
|
deleteMessage(message.SendTime);
|
||||||
|
} else {
|
||||||
|
submitEditMessage(editDraft);
|
||||||
|
}
|
||||||
|
setEditDraft(message.Content);
|
||||||
|
setEditing(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="link"
|
||||||
|
className="text-muted-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
setEditDraft(message.Content);
|
||||||
|
setEditing(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : isValidURL ? (
|
||||||
|
<Media link={message.Content} />
|
||||||
|
) : (
|
||||||
|
<Text value={message.Content} />
|
||||||
|
)}
|
||||||
|
{groupedReactions.length > 0 && (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-1 pb-1">
|
||||||
|
{groupedReactions.map(
|
||||||
|
([reaction, { count, reactedByMe }]) => (
|
||||||
|
<Button
|
||||||
|
key={reaction}
|
||||||
|
aria-label={`${reactedByMe ? "Remove" : "Add"} ${reaction} reaction`}
|
||||||
|
className={cn(
|
||||||
|
"h-7 gap-2 rounded-lg py-3.5 px-1.5! border",
|
||||||
|
reactedByMe
|
||||||
|
? "border-(--primary-foreground-alt)/40!"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
onClick={() => void toggleReaction(reaction)}
|
||||||
|
size="xs"
|
||||||
|
variant={reactedByMe ? "subtleDefault" : "outline"}
|
||||||
|
>
|
||||||
|
<Emoji className="h-5 w-5" shortcode={reaction} />
|
||||||
|
<span className="text-sm">{count}</span>
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
{editing ? (
|
</>
|
||||||
<div className="flex w-full flex-col">
|
</div>
|
||||||
<Input
|
</>
|
||||||
className="w-full"
|
|
||||||
styled
|
|
||||||
setValue={setEditDraft}
|
|
||||||
value={editDraft}
|
|
||||||
onSubmit={() => {
|
|
||||||
submitEditMessage(editDraft);
|
|
||||||
setEditing(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex">
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="link"
|
|
||||||
className="text-primary-foreground-alt"
|
|
||||||
onClick={() => {
|
|
||||||
if (editDraft === message.Content) {
|
|
||||||
deleteMessage(message.SendTime);
|
|
||||||
} else {
|
|
||||||
submitEditMessage(editDraft);
|
|
||||||
}
|
|
||||||
setEditDraft(message.Content);
|
|
||||||
setEditing(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="link"
|
|
||||||
className="text-muted-foreground"
|
|
||||||
onClick={() => {
|
|
||||||
setEditDraft(message.Content);
|
|
||||||
setEditing(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : isValidURL ? (
|
|
||||||
<Media link={message.Content} />
|
|
||||||
) : (
|
|
||||||
<Text value={message.Content} />
|
|
||||||
)}
|
|
||||||
{groupedReactions.length > 0 && (
|
|
||||||
<div className="mt-1 flex flex-wrap gap-1 pb-1">
|
|
||||||
{groupedReactions.map(
|
|
||||||
([reaction, { count, reactedByMe }]) => (
|
|
||||||
<Button
|
|
||||||
key={reaction}
|
|
||||||
aria-label={`${reactedByMe ? "Remove" : "Add"} ${reaction} reaction`}
|
|
||||||
className={cn(
|
|
||||||
"h-7 gap-2 rounded-lg py-3.5 px-1.5! border",
|
|
||||||
reactedByMe
|
|
||||||
? "border-(--primary-foreground-alt)/40!"
|
|
||||||
: "",
|
|
||||||
)}
|
|
||||||
onClick={() => void toggleReaction(reaction)}
|
|
||||||
size="xs"
|
|
||||||
variant={reactedByMe ? "subtleDefault" : "outline"}
|
|
||||||
>
|
|
||||||
<Emoji className="h-5 w-5" shortcode={reaction} />
|
|
||||||
<span className="text-sm">{count}</span>
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
</MessageContextMenu>
|
</MessageContextMenu>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="h-10 w-30" />
|
<Skeleton className="h-10 w-30" />
|
||||||
|
|
@ -388,6 +446,7 @@ export default memo(MessageComponent, (prev, next) => {
|
||||||
prev.message.SendTime === next.message.SendTime &&
|
prev.message.SendTime === next.message.SendTime &&
|
||||||
prev.message.Content === next.message.Content &&
|
prev.message.Content === next.message.Content &&
|
||||||
prev.message.SenderId === next.message.SenderId &&
|
prev.message.SenderId === next.message.SenderId &&
|
||||||
|
prev.message.ReplyId === next.message.ReplyId &&
|
||||||
prev.message.MessageState === next.message.MessageState &&
|
prev.message.MessageState === next.message.MessageState &&
|
||||||
prev.message.failed === next.message.failed &&
|
prev.message.failed === next.message.failed &&
|
||||||
prev.message.decryptionFailed === next.message.decryptionFailed &&
|
prev.message.decryptionFailed === next.message.decryptionFailed &&
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,13 @@ import {
|
||||||
Reply,
|
Reply,
|
||||||
Trash,
|
Trash,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { cloneElement, useMemo, useState, useSyncExternalStore } from "react";
|
import { cloneElement, useMemo, useState, useSyncExternalStore } from "react";
|
||||||
import type {
|
import type {
|
||||||
MouseEvent as ReactMouseEvent,
|
MouseEvent as ReactMouseEvent,
|
||||||
ReactElement,
|
ReactElement,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
Ref,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useChat } from "../context";
|
import { useChat } from "../context";
|
||||||
import Emoji from "@tensamin/markdown/emoji";
|
import Emoji from "@tensamin/markdown/emoji";
|
||||||
|
|
@ -260,18 +262,32 @@ function MiniMenuTooltip({
|
||||||
children,
|
children,
|
||||||
label,
|
label,
|
||||||
}: {
|
}: {
|
||||||
children: ReactElement;
|
children: ReactElement<{
|
||||||
|
onClick?: (event: ReactMouseEvent<HTMLElement>) => void;
|
||||||
|
ref?: Ref<HTMLElement>;
|
||||||
|
}>;
|
||||||
label: string;
|
label: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger render={children} />
|
<TooltipTrigger
|
||||||
|
render={({ ref, onClick }) =>
|
||||||
|
cloneElement(children, {
|
||||||
|
ref,
|
||||||
|
onClick: (event) => {
|
||||||
|
onClick?.(event);
|
||||||
|
children.props.onClick?.(event);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>{label}</TooltipContent>
|
<TooltipContent>{label}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MiniMessageMenu({
|
function MiniMessageMenu({
|
||||||
|
fadeOut,
|
||||||
isOwnMessage,
|
isOwnMessage,
|
||||||
onDelete,
|
onDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
|
@ -283,6 +299,7 @@ function MiniMessageMenu({
|
||||||
onReply,
|
onReply,
|
||||||
shiftIsPressed,
|
shiftIsPressed,
|
||||||
}: {
|
}: {
|
||||||
|
fadeOut: boolean;
|
||||||
isOwnMessage: boolean;
|
isOwnMessage: boolean;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
|
|
@ -295,8 +312,12 @@ function MiniMessageMenu({
|
||||||
shiftIsPressed: boolean;
|
shiftIsPressed: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute right-4 -top-3 z-10">
|
<motion.div
|
||||||
<Card className="flex flex-row justify-end gap-0! rounded-lg! p-0! shadow-lg">
|
className="absolute right-4 -top-3 z-10"
|
||||||
|
exit={fadeOut ? { opacity: 0 } : undefined}
|
||||||
|
transition={fadeOut ? { duration: 0.048 } : undefined}
|
||||||
|
>
|
||||||
|
<Card className="flex flex-row justify-end gap-0! rounded-lg! p-px! shadow-lg">
|
||||||
{quickReactions.map((emoji) => (
|
{quickReactions.map((emoji) => (
|
||||||
<MiniMenuTooltip
|
<MiniMenuTooltip
|
||||||
key={emoji}
|
key={emoji}
|
||||||
|
|
@ -319,15 +340,16 @@ function MiniMessageMenu({
|
||||||
children={
|
children={
|
||||||
usePickerTrigger ? (
|
usePickerTrigger ? (
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
render={
|
render={({ onClick }) => (
|
||||||
<Button
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
aria-label="Add reaction"
|
aria-label="Add reaction"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
>
|
>
|
||||||
<Laugh />
|
<Laugh />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -397,7 +419,7 @@ function MiniMessageMenu({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -538,8 +560,11 @@ export default function MessageContextMenu({
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const [mainDrawerOpen, setMainDrawerOpen] = useState(false);
|
const [mainDrawerOpen, setMainDrawerOpen] = useState(false);
|
||||||
|
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||||
const [reactionDrawerOpen, setReactionDrawerOpen] = useState(false);
|
const [reactionDrawerOpen, setReactionDrawerOpen] = useState(false);
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
|
const [pickerClosing, setPickerClosing] = useState(false);
|
||||||
|
const [fadeMiniMenu, setFadeMiniMenu] = useState(false);
|
||||||
const { ranks } = useEmojiRanks();
|
const { ranks } = useEmojiRanks();
|
||||||
const quickReactions = getRecentEmojis(ranks, 3);
|
const quickReactions = getRecentEmojis(ranks, 3);
|
||||||
const menuReactions = getRecentEmojis(ranks, 5);
|
const menuReactions = getRecentEmojis(ranks, 5);
|
||||||
|
|
@ -547,7 +572,11 @@ export default function MessageContextMenu({
|
||||||
function selectReaction(emoji: string) {
|
function selectReaction(emoji: string) {
|
||||||
void onReact(emoji);
|
void onReact(emoji);
|
||||||
setReactionDrawerOpen(false);
|
setReactionDrawerOpen(false);
|
||||||
setPickerOpen(false);
|
setPickerVisibility(false);
|
||||||
|
}
|
||||||
|
function setPickerVisibility(open: boolean) {
|
||||||
|
setPickerOpen(open);
|
||||||
|
setPickerClosing(!isMobile && !open);
|
||||||
}
|
}
|
||||||
const mainDrawerComponents = useMemo(
|
const mainDrawerComponents = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -578,20 +607,28 @@ export default function MessageContextMenu({
|
||||||
onPointerLeave={scheduleMiniMenuClose}
|
onPointerLeave={scheduleMiniMenuClose}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{!isMobile && !hideMiniMenu && activeMenuId === messageId && (
|
<AnimatePresence onExitComplete={() => setFadeMiniMenu(false)}>
|
||||||
<MiniMessageMenu
|
{!isMobile &&
|
||||||
isOwnMessage={isOwnMessage}
|
!hideMiniMenu &&
|
||||||
onDelete={() => deleteMessage(messageId)}
|
(activeMenuId === messageId ||
|
||||||
onEdit={() => onSetEditing(true)}
|
contextMenuOpen ||
|
||||||
onOpenMenu={openMenu}
|
pickerOpen ||
|
||||||
onOpenPicker={() => setPickerOpen(true)}
|
pickerClosing) && (
|
||||||
usePickerTrigger={!isMobile}
|
<MiniMessageMenu
|
||||||
onReact={selectReaction}
|
fadeOut={fadeMiniMenu}
|
||||||
quickReactions={quickReactions}
|
isOwnMessage={isOwnMessage}
|
||||||
onReply={() => setReplyTo(messageId)}
|
onDelete={() => deleteMessage(messageId)}
|
||||||
shiftIsPressed={shiftIsPressed}
|
onEdit={() => onSetEditing(true)}
|
||||||
/>
|
onOpenMenu={openMenu}
|
||||||
)}
|
onOpenPicker={() => setPickerVisibility(true)}
|
||||||
|
usePickerTrigger={!isMobile}
|
||||||
|
onReact={selectReaction}
|
||||||
|
quickReactions={quickReactions}
|
||||||
|
onReply={() => setReplyTo(messageId)}
|
||||||
|
shiftIsPressed={shiftIsPressed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -636,14 +673,14 @@ export default function MessageContextMenu({
|
||||||
emojis={menuReactions}
|
emojis={menuReactions}
|
||||||
onMore={() => {
|
onMore={() => {
|
||||||
setReactionDrawerOpen(false);
|
setReactionDrawerOpen(false);
|
||||||
setPickerOpen(true);
|
setPickerVisibility(true);
|
||||||
}}
|
}}
|
||||||
onSelect={selectReaction}
|
onSelect={selectReaction}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<Drawer open={pickerOpen} onOpenChange={setPickerOpen}>
|
<Drawer open={pickerOpen} onOpenChange={setPickerVisibility}>
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerTitle className="sr-only">Choose an emoji</DrawerTitle>
|
<DrawerTitle className="sr-only">Choose an emoji</DrawerTitle>
|
||||||
<DrawerDescription className="sr-only">
|
<DrawerDescription className="sr-only">
|
||||||
|
|
@ -672,8 +709,17 @@ export default function MessageContextMenu({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={pickerOpen} onOpenChange={setPickerOpen}>
|
<Popover
|
||||||
<ContextMenu>
|
open={pickerOpen}
|
||||||
|
onOpenChange={(open, eventDetails) => {
|
||||||
|
setPickerVisibility(open);
|
||||||
|
setFadeMiniMenu(!open && eventDetails.reason === "outside-press");
|
||||||
|
}}
|
||||||
|
onOpenChangeComplete={(open) => {
|
||||||
|
if (!open) setPickerClosing(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContextMenu onOpenChange={setContextMenuOpen}>
|
||||||
<ContextMenuTrigger render={child} />
|
<ContextMenuTrigger render={child} />
|
||||||
<MessageMenuContent
|
<MessageMenuContent
|
||||||
components={desktopMenuComponents}
|
components={desktopMenuComponents}
|
||||||
|
|
@ -681,7 +727,7 @@ export default function MessageContextMenu({
|
||||||
devEnabled={devEnabled}
|
devEnabled={devEnabled}
|
||||||
isOwnMessage={isOwnMessage}
|
isOwnMessage={isOwnMessage}
|
||||||
messageId={messageId}
|
messageId={messageId}
|
||||||
onAddReaction={() => setPickerOpen(true)}
|
onAddReaction={() => setPickerVisibility(true)}
|
||||||
onReact={selectReaction}
|
onReact={selectReaction}
|
||||||
reactionEmojis={menuReactions}
|
reactionEmojis={menuReactions}
|
||||||
onSetEditing={onSetEditing}
|
onSetEditing={onSetEditing}
|
||||||
|
|
|
||||||
97
packages/chat/src/components/replyBox.tsx
Normal file
97
packages/chat/src/components/replyBox.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
Button,
|
||||||
|
cn,
|
||||||
|
Skeleton,
|
||||||
|
} from "@methanium/ui";
|
||||||
|
import Text from "@tensamin/markdown/text";
|
||||||
|
import type { User } from "@tensamin/user/context";
|
||||||
|
import Wrapper from "@tensamin/user/wrapper";
|
||||||
|
import { Forward, X } from "lucide-react";
|
||||||
|
|
||||||
|
function ReplyUser({ user }: { user: User }) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
<Avatar className="h-5 w-5 shrink-0">
|
||||||
|
<AvatarImage src={user.Avatar} />
|
||||||
|
<AvatarFallback className="text-[0.6rem]">
|
||||||
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<p className="text-sm font-semibold text-muted-foreground">
|
||||||
|
{user.Display}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReplyBox({
|
||||||
|
content,
|
||||||
|
loading = false,
|
||||||
|
onDismiss,
|
||||||
|
user,
|
||||||
|
userId,
|
||||||
|
variant,
|
||||||
|
}: {
|
||||||
|
content?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
onDismiss?: () => void;
|
||||||
|
user?: User;
|
||||||
|
userId?: number;
|
||||||
|
variant: "composer" | "message";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
variant === "composer" ? "flex justify-center" : "pl-5 w-full",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex w-[90%] items-center gap-1 text-xs",
|
||||||
|
variant === "composer"
|
||||||
|
? "justify-start rounded-t-lg border-x border-t border-(--border)/65 bg-background p-1"
|
||||||
|
: "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Forward className="shrink-0 opacity-60" size={18} />
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Skeleton className="h-5 w-5 shrink-0 rounded-full" />
|
||||||
|
<Skeleton className="h-5 w-full" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{user ? (
|
||||||
|
<ReplyUser user={user} />
|
||||||
|
) : userId ? (
|
||||||
|
<Wrapper
|
||||||
|
userId={userId}
|
||||||
|
loading={<Skeleton className="h-5 w-5 rounded-full" />}
|
||||||
|
component={(resolvedUser) => <ReplyUser user={resolvedUser} />}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{content !== undefined && (
|
||||||
|
<div className="h-5.5 min-w-0 flex-1 truncate">
|
||||||
|
<Text fontSize="0.88rem" value={content} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{onDismiss && (
|
||||||
|
<Button
|
||||||
|
aria-label="Cancel reply"
|
||||||
|
className="ml-auto h-5 w-5 rounded-md! p-0!"
|
||||||
|
onClick={onDismiss}
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<X className="opacity-60" size={18} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -119,8 +119,45 @@ type SendMessageGet = (
|
||||||
|
|
||||||
type GetChatSecret = (userId: number) => Promise<Uint8Array | null>;
|
type GetChatSecret = (userId: number) => Promise<Uint8Array | null>;
|
||||||
|
|
||||||
|
export async function getMessage({
|
||||||
|
sendTime,
|
||||||
|
ownId,
|
||||||
|
chatPartnerId,
|
||||||
|
send,
|
||||||
|
}: {
|
||||||
|
sendTime: number;
|
||||||
|
ownId: number;
|
||||||
|
chatPartnerId: number;
|
||||||
|
send: SendMessageGet;
|
||||||
|
}): Promise<RawMessage> {
|
||||||
|
const cache = createCache(String(ownId), {
|
||||||
|
codec: secureValueCodec,
|
||||||
|
});
|
||||||
|
const window = await cache.conversations.get(chatPartnerId);
|
||||||
|
const cachedMessage = window?.Messages.find(
|
||||||
|
(message) => message.SendTime === sendTime,
|
||||||
|
);
|
||||||
|
const message =
|
||||||
|
cachedMessage ??
|
||||||
|
(
|
||||||
|
await send("MessageGet", {
|
||||||
|
SendTime: sendTime,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
const messageChatPartnerId =
|
||||||
|
message.SenderId === ownId ? chatPartnerId : message.SenderId;
|
||||||
|
|
||||||
|
if (messageChatPartnerId !== chatPartnerId) {
|
||||||
|
throw new Error("Reply message does not belong to this chat");
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchReplyMessage({
|
export async function fetchReplyMessage({
|
||||||
replyTo,
|
replyTo,
|
||||||
|
ownId,
|
||||||
|
chatUserId,
|
||||||
send,
|
send,
|
||||||
getChatSecret,
|
getChatSecret,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -130,22 +167,22 @@ export async function fetchReplyMessage({
|
||||||
send: SendMessageGet;
|
send: SendMessageGet;
|
||||||
getChatSecret: GetChatSecret;
|
getChatSecret: GetChatSecret;
|
||||||
}) {
|
}) {
|
||||||
const value = await send("MessageGet", {
|
const message = await getMessage({
|
||||||
SendTime: replyTo,
|
sendTime: replyTo,
|
||||||
|
ownId,
|
||||||
|
chatPartnerId: chatUserId,
|
||||||
|
send,
|
||||||
});
|
});
|
||||||
const chatSecret = await getChatSecret(value.data.SenderId);
|
const chatSecret = await getChatSecret(chatUserId);
|
||||||
|
|
||||||
if (!chatSecret) {
|
if (!chatSecret) {
|
||||||
throw new Error("Missing chat secret");
|
throw new Error("Missing chat secret");
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedContent = await decryptChatText(
|
const decryptedContent = await decryptChatText(chatSecret, message.Content);
|
||||||
chatSecret,
|
|
||||||
value.data.Content,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...value.data,
|
...message,
|
||||||
Content: decryptedContent,
|
Content: decryptedContent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -791,6 +828,9 @@ export default function Provider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
// Replys
|
// Replys
|
||||||
const [replyTo, setReplyTo] = useState<number | undefined>(undefined);
|
const [replyTo, setReplyTo] = useState<number | undefined>(undefined);
|
||||||
|
useEffect(() => {
|
||||||
|
setReplyTo(undefined);
|
||||||
|
}, [userIdValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|
|
||||||
|
|
@ -478,6 +478,7 @@ export default function Screen() {
|
||||||
const lastMessage = messages[messageIndex - 1];
|
const lastMessage = messages[messageIndex - 1];
|
||||||
const isGrouped =
|
const isGrouped =
|
||||||
lastMessage &&
|
lastMessage &&
|
||||||
|
!message.ReplyId &&
|
||||||
lastMessage.SenderId === message.SenderId &&
|
lastMessage.SenderId === message.SenderId &&
|
||||||
Math.round(lastMessage.SendTime / 10000) ===
|
Math.round(lastMessage.SendTime / 10000) ===
|
||||||
Math.round(message.SendTime / 10000);
|
Math.round(message.SendTime / 10000);
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,7 @@
|
||||||
- Confirmation when exiting with text in the input box.
|
- Confirmation when exiting with text in the input box.
|
||||||
- Add arrow up hotkey to edit last message (req: packages/hotkeys)
|
- Add arrow up hotkey to edit last message (req: packages/hotkeys)
|
||||||
- Drop any unique reactions above 10
|
- Drop any unique reactions above 10
|
||||||
- Make the emoji picker not get moved with the mini context menu
|
- Add proper loading skeleton for individual messages
|
||||||
- Add proper loading skeleton
|
- Sounds
|
||||||
|
- Reply jumping
|
||||||
|
- Add emoji picker
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo, type CSSProperties } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ensureMarkdownStyles,
|
ensureMarkdownStyles,
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
|
|
||||||
export type TextProps = {
|
export type TextProps = {
|
||||||
value: string;
|
value: string;
|
||||||
|
fontSize?: CSSProperties["fontSize"];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -21,5 +22,9 @@ export default function Text(props: TextProps) {
|
||||||
const blocks = useMemo(() => parseMarkdownBlocks(props.value), [props.value]);
|
const blocks = useMemo(() => parseMarkdownBlocks(props.value), [props.value]);
|
||||||
const renderedBlocks = useMemo(() => renderBlocks(blocks), [blocks]);
|
const renderedBlocks = useMemo(() => renderBlocks(blocks), [blocks]);
|
||||||
|
|
||||||
return <div className="tm-md-root">{renderedBlocks}</div>;
|
return (
|
||||||
|
<div className="tm-md-root" style={{ fontSize: props.fontSize }}>
|
||||||
|
{renderedBlocks}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Outlet, useLocation, useNavigate } from "@tanstack/react-router";
|
import { Outlet, useLocation, useNavigate } from "@tanstack/react-router";
|
||||||
import { Button, ClearStorageButton, cn, useIsMobile } from "@methanium/ui";
|
import { Button, ClearStorageButton, cn, useIsMobile } from "@methanium/ui";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
import { settingsNavigation } from "./manifest";
|
import { settingsNavigation } from "./manifest";
|
||||||
|
|
||||||
|
|
@ -8,24 +9,41 @@ export default function SettingsLayout() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full">
|
<div className="flex h-full min-h-0 w-full">
|
||||||
{!isMobile && <SettingsSidebar />}
|
<SettingsSidebar className="hidden md:flex" />
|
||||||
<div className="bg-background w-full h-full p-3 flex flex-col gap-3">
|
<div className="bg-background flex h-full min-h-0 w-full flex-col gap-3 overflow-y-auto p-3">
|
||||||
<h1 className="text-xl font-semibold">
|
<div className="flex items-center gap-2">
|
||||||
{location.pathname
|
{isMobile && !/^\/settings\/?$/.test(location.pathname) && (
|
||||||
.split("/")
|
<Button
|
||||||
.pop()
|
aria-label="Go back"
|
||||||
?.replace(/-/g, " ")
|
className="h-8 w-8 shrink-0 p-0"
|
||||||
.replace(/\b\w/g, (letter) => letter.toUpperCase())}
|
variant="ghost"
|
||||||
</h1>
|
onClick={() => window.history.back()}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<h1 className="text-xl font-semibold">
|
||||||
|
{location.pathname
|
||||||
|
.split("/")
|
||||||
|
.pop()
|
||||||
|
?.replace(/-/g, " ")
|
||||||
|
.replace(/\b\w/g, (letter) => letter.toUpperCase())}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsSidebar() {
|
export function SettingsSidebar({
|
||||||
const isMobile = useIsMobile();
|
mobile = false,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
mobile?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const categories = [
|
const categories = [
|
||||||
...new Set(settingsNavigation.map((page) => page.category)),
|
...new Set(settingsNavigation.map((page) => page.category)),
|
||||||
|
|
@ -34,10 +52,11 @@ export function SettingsSidebar() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
isMobile
|
mobile
|
||||||
? "w-full p-1"
|
? "w-full p-1"
|
||||||
: "p-3 rounded-tl-2xl border-r bg-input/15 w-50",
|
: "p-3 rounded-tl-2xl border-r bg-input/15 w-50",
|
||||||
"flex flex-col gap-6",
|
"flex flex-col gap-6",
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import Theme from "./pages/theme";
|
||||||
|
|
||||||
export const settingsPages = [
|
export const settingsPages = [
|
||||||
{ path: "/", component: Index },
|
{ path: "/", component: Index },
|
||||||
{ category: "general", path: "chat", label: "Chat", component: Chat },
|
|
||||||
{
|
{
|
||||||
category: "account",
|
category: "account",
|
||||||
path: "profile",
|
path: "profile",
|
||||||
|
|
@ -21,6 +20,7 @@ export const settingsPages = [
|
||||||
label: "Security",
|
label: "Security",
|
||||||
component: Security,
|
component: Security,
|
||||||
},
|
},
|
||||||
|
{ category: "general", path: "chat", label: "Chat", component: Chat },
|
||||||
{ category: "application", path: "cache", label: "Cache", component: Cache },
|
{ category: "application", path: "cache", label: "Cache", component: Cache },
|
||||||
{ category: "application", path: "theme", label: "Theme", component: Theme },
|
{ category: "application", path: "theme", label: "Theme", component: Theme },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { useIsMobile } from "@methanium/ui";
|
|
||||||
import { SettingsSidebar } from "../layout";
|
import { SettingsSidebar } from "../layout";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const isMobile = useIsMobile();
|
return (
|
||||||
return isMobile && <SettingsSidebar />;
|
<div className="h-full md:hidden">
|
||||||
|
<SettingsSidebar mobile />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export default function Page() {
|
||||||
<p>Package Count: {packageCount}</p>
|
<p>Package Count: {packageCount}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 flex-1 max-h-[calc(100vh-180px)] overflow-auto pr-2">
|
<div className="min-h-0 flex-1 max-h-[calc(100vh-180px)] overflow-auto pr-2">
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5 p-px">
|
||||||
{packages.map((licensePackage) => (
|
{packages.map((licensePackage) => (
|
||||||
<Card
|
<Card
|
||||||
className="pb-0!"
|
className="pb-0!"
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ const settings = {
|
||||||
receive_confirmations: {
|
receive_confirmations: {
|
||||||
display: "Enable Receive Confirmations",
|
display: "Enable Receive Confirmations",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: true,
|
default: false,
|
||||||
},
|
},
|
||||||
show_start_of_last_message_in_sidebar: {
|
show_start_of_last_message_in_sidebar: {
|
||||||
display: "Show Start of Last Message in Sidebar",
|
display: "Show Start of Last Message in Sidebar",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ import { useSession } from "@tensamin/storage/session";
|
||||||
|
|
||||||
export type User = z.infer<typeof schemas.GetUserData.response>;
|
export type User = z.infer<typeof schemas.GetUserData.response>;
|
||||||
|
|
||||||
|
const USER_CACHE_MAX_AGE = 5 * 60 * 1000;
|
||||||
|
|
||||||
interface contextValue {
|
interface contextValue {
|
||||||
get(userId: number): Promise<User>;
|
get(userId: number): Promise<User>;
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +33,7 @@ const UserContext = createContext<contextValue | undefined>(undefined);
|
||||||
export default function UserProvider(props: { children: ReactNode }) {
|
export default function UserProvider(props: { children: ReactNode }) {
|
||||||
const storageRef = useRef<Record<number, User>>({});
|
const storageRef = useRef<Record<number, User>>({});
|
||||||
const pendingRef = useRef<Record<number, Promise<User> | undefined>>({});
|
const pendingRef = useRef<Record<number, Promise<User> | undefined>>({});
|
||||||
|
const checkedAtRef = useRef<Record<number, number>>({});
|
||||||
|
|
||||||
const { send } = useMTP();
|
const { send } = useMTP();
|
||||||
const { load } = useStorage();
|
const { load } = useStorage();
|
||||||
|
|
@ -66,17 +69,36 @@ export default function UserProvider(props: { children: ReactNode }) {
|
||||||
const cachedResult =
|
const cachedResult =
|
||||||
schemas.GetUserData.response.safeParse(cachedValue);
|
schemas.GetUserData.response.safeParse(cachedValue);
|
||||||
const cached = cachedResult.success ? cachedResult.data : undefined;
|
const cached = cachedResult.success ? cachedResult.data : undefined;
|
||||||
if (cached) storageRef.current[userId] = cached;
|
if (cached) {
|
||||||
|
storageRef.current[userId] = cached;
|
||||||
|
const checkedAt = checkedAtRef.current[userId];
|
||||||
|
if (checkedAt && Date.now() - checkedAt < USER_CACHE_MAX_AGE) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const userData = await send("GetUserData", { UserId: userId });
|
const userData = await send("GetUserData", { UserId: userId });
|
||||||
|
if (
|
||||||
|
userData.type === "ErrorNotFound" ||
|
||||||
|
userData.data.UserId === 0
|
||||||
|
) {
|
||||||
|
delete storageRef.current[userId];
|
||||||
|
delete checkedAtRef.current[userId];
|
||||||
|
await cache?.profiles.delete(userId);
|
||||||
|
throw new Error("GetUserData failed: user not found");
|
||||||
|
}
|
||||||
if (userData.type.startsWith("Error")) {
|
if (userData.type.startsWith("Error")) {
|
||||||
throw new Error(`GetUserData failed: ${userData.type}`);
|
throw new Error(`GetUserData failed: ${userData.type}`);
|
||||||
}
|
}
|
||||||
const user = userData.data;
|
const user = userData.data;
|
||||||
storageRef.current[userId] = user;
|
storageRef.current[userId] = user;
|
||||||
|
checkedAtRef.current[userId] = Date.now();
|
||||||
return user;
|
return user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (cached) return cached;
|
if (cached && storageRef.current[userId]) {
|
||||||
|
checkedAtRef.current[userId] = Date.now();
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
|
|
@ -5,7 +5,7 @@ settings:
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
'@methanium/ui': https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
'@methanium/ui': https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
mtp: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz
|
mtp: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
@ -13,8 +13,8 @@ importers:
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
mtp:
|
mtp:
|
||||||
specifier: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz
|
specifier: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz
|
||||||
version: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz
|
version: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz
|
||||||
|
|
@ -89,8 +89,8 @@ importers:
|
||||||
apps/tauri:
|
apps/tauri:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.11.1
|
version: 2.11.1
|
||||||
|
|
@ -153,8 +153,8 @@ importers:
|
||||||
specifier: ^5.2.7
|
specifier: ^5.2.7
|
||||||
version: 5.2.7
|
version: 5.2.7
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@radix-ui/primitive':
|
'@radix-ui/primitive':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.4
|
version: 1.1.4
|
||||||
|
|
@ -532,8 +532,8 @@ importers:
|
||||||
specifier: ^2.9.20
|
specifier: ^2.9.20
|
||||||
version: 2.9.21(livekit-client@2.20.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tslib@2.8.1)
|
version: 2.9.21(livekit-client@2.20.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tslib@2.8.1)
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@tanstack/react-router':
|
'@tanstack/react-router':
|
||||||
specifier: ^1.169.1
|
specifier: ^1.169.1
|
||||||
version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
|
@ -583,8 +583,8 @@ importers:
|
||||||
packages/chat:
|
packages/chat:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@tanstack/pacer':
|
'@tanstack/pacer':
|
||||||
specifier: ^0.21.1
|
specifier: ^0.21.1
|
||||||
version: 0.21.1
|
version: 0.21.1
|
||||||
|
|
@ -664,8 +664,8 @@ importers:
|
||||||
specifier: ^6.41.1
|
specifier: ^6.41.1
|
||||||
version: 6.43.4
|
version: 6.43.4
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@twemoji/api':
|
'@twemoji/api':
|
||||||
specifier: ^17.0.3
|
specifier: ^17.0.3
|
||||||
version: 17.0.3
|
version: 17.0.3
|
||||||
|
|
@ -682,8 +682,8 @@ importers:
|
||||||
packages/mtp:
|
packages/mtp:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.11.1
|
version: 2.11.1
|
||||||
|
|
@ -722,8 +722,8 @@ importers:
|
||||||
packages/notifications:
|
packages/notifications:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@tanstack/react-router':
|
'@tanstack/react-router':
|
||||||
specifier: ^1.169.1
|
specifier: ^1.169.1
|
||||||
version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
|
@ -764,8 +764,8 @@ importers:
|
||||||
packages/onboarding:
|
packages/onboarding:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@tensamin/shared':
|
'@tensamin/shared':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../shared
|
version: link:../shared
|
||||||
|
|
@ -788,8 +788,8 @@ importers:
|
||||||
packages/settings:
|
packages/settings:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@tanstack/react-router':
|
'@tanstack/react-router':
|
||||||
specifier: ^1.169.1
|
specifier: ^1.169.1
|
||||||
version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
|
@ -828,8 +828,8 @@ importers:
|
||||||
packages/shared:
|
packages/shared:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.14.0
|
specifier: ^1.14.0
|
||||||
version: 1.23.0(react@19.2.7)
|
version: 1.23.0(react@19.2.7)
|
||||||
|
|
@ -846,8 +846,8 @@ importers:
|
||||||
packages/storage:
|
packages/storage:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.11.1
|
version: 2.11.1
|
||||||
|
|
@ -870,8 +870,8 @@ importers:
|
||||||
packages/tauth:
|
packages/tauth:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@methanium/ui':
|
'@methanium/ui':
|
||||||
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz
|
specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz
|
||||||
version: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
'@tanstack/react-router':
|
'@tanstack/react-router':
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
|
@ -1443,9 +1443,6 @@ packages:
|
||||||
'@fontsource-variable/public-sans@5.2.7':
|
'@fontsource-variable/public-sans@5.2.7':
|
||||||
resolution: {integrity: sha512-4mvade2J3slKkvwRkS+p8T3szet/0vhWoSnuUJTVU81Uo2pRpSZY/Y8bSLRqpSwzIPxjVmRJ53oq6JKP/l/PSg==}
|
resolution: {integrity: sha512-4mvade2J3slKkvwRkS+p8T3szet/0vhWoSnuUJTVU81Uo2pRpSZY/Y8bSLRqpSwzIPxjVmRJ53oq6JKP/l/PSg==}
|
||||||
|
|
||||||
'@fontsource-variable/public-sans@5.3.0':
|
|
||||||
resolution: {integrity: sha512-AVfkmAt50BMXWpOO21FAntiJFKGX6xTc2dSL8dxtDteONe9IuRXJWGbs0EbG955vAMCq23ENeuopuW87cGWDSQ==}
|
|
||||||
|
|
||||||
'@hono/node-server@1.19.14':
|
'@hono/node-server@1.19.14':
|
||||||
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
|
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
|
||||||
engines: {node: '>=18.14.1'}
|
engines: {node: '>=18.14.1'}
|
||||||
|
|
@ -1550,9 +1547,9 @@ packages:
|
||||||
'@marijn/find-cluster-break@1.0.3':
|
'@marijn/find-cluster-break@1.0.3':
|
||||||
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
|
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
|
||||||
|
|
||||||
'@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz':
|
'@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz':
|
||||||
resolution: {integrity: sha512-I71TI1q0ykINusqZH41h5qfOuVLQPXrFa8AVFKKKCMSnIcbk0rWtrTdSQWj/fQZfOAk3f8+f5KT6EQ/lDVkw0A==, tarball: https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz}
|
resolution: {integrity: sha512-o/IWQyE8UzBFRXcOcBLKMKl6SSjw1HLczvhB1snFH19DyVbLGTHkZ8Ty7fOAu36/8juQJeWEMurtKmYqJO6XkA==, tarball: https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz}
|
||||||
version: 0.0.10
|
version: 0.0.22
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.2.7
|
react: ^19.2.7
|
||||||
react-dom: ^19.2.7
|
react-dom: ^19.2.7
|
||||||
|
|
@ -5746,8 +5743,6 @@ snapshots:
|
||||||
|
|
||||||
'@fontsource-variable/public-sans@5.2.7': {}
|
'@fontsource-variable/public-sans@5.2.7': {}
|
||||||
|
|
||||||
'@fontsource-variable/public-sans@5.3.0': {}
|
|
||||||
|
|
||||||
'@hono/node-server@1.19.14(hono@4.12.27)':
|
'@hono/node-server@1.19.14(hono@4.12.27)':
|
||||||
dependencies:
|
dependencies:
|
||||||
hono: 4.12.27
|
hono: 4.12.27
|
||||||
|
|
@ -5865,10 +5860,9 @@ snapshots:
|
||||||
|
|
||||||
'@marijn/find-cluster-break@1.0.3': {}
|
'@marijn/find-cluster-break@1.0.3': {}
|
||||||
|
|
||||||
'@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)':
|
'@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@base-ui/react': 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
'@base-ui/react': 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@fontsource-variable/public-sans': 5.3.0
|
|
||||||
'@tauri-apps/api': 2.11.1
|
'@tauri-apps/api': 2.11.1
|
||||||
class-variance-authority: 0.7.1
|
class-variance-authority: 0.7.1
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,5 @@ allowBuilds:
|
||||||
electron-winstaller: true
|
electron-winstaller: true
|
||||||
esbuild: true
|
esbuild: true
|
||||||
overrides:
|
overrides:
|
||||||
"@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.10/methanium-ui.tgz"
|
"@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.22/methanium-ui.tgz"
|
||||||
mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz"
|
mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.2.0-dev-a692bed/mtp-0.2.0.tgz"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue