(feat): add reply box to messages
All checks were successful
/ build-web (push) Successful in 7m40s
/ build-desktop (linux) (push) Successful in 12m6s
/ build-mobile (push) Successful in 19m4s
/ release (push) Successful in 3m25s

(feat): update methanium ui
(fix): user data caching
(fix): other random stuff
(qol): update todo
This commit is contained in:
Alois 2026-07-30 23:01:42 +02:00
commit 6a5236bafe
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
32 changed files with 722 additions and 394 deletions

View file

@ -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>

View file

@ -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)}
> >

View file

@ -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" />

View file

@ -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,

View file

@ -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">

View file

@ -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>

View file

@ -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

View file

@ -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);
} }

View file

@ -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}>

View file

@ -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);
} }

View file

@ -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);
} }

View file

@ -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

View file

@ -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>
);
}

View file

@ -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",

View file

@ -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;
} }

View file

@ -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,
}); });
}) })

View file

@ -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}

View file

@ -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 &&

View file

@ -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}

View 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>
);
}

View file

@ -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}>

View file

@ -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);

View file

@ -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

View file

@ -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>
);
} }

View file

@ -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) => (

View file

@ -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 },
{ {

View file

@ -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>
);
} }

View file

@ -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!"

View file

@ -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",

View file

@ -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
View file

@ -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

View file

@ -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"