diff --git a/apps/web/src/components/modals/basic.tsx b/apps/web/src/components/modals/basic.tsx index 8a0a611..46eaa45 100644 --- a/apps/web/src/components/modals/basic.tsx +++ b/apps/web/src/components/modals/basic.tsx @@ -29,7 +29,7 @@ export function Basic({ render={
} nativeButton={false} 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!" >
diff --git a/apps/web/src/components/modals/profile.tsx b/apps/web/src/components/modals/profile.tsx index 2328228..edd83a6 100644 --- a/apps/web/src/components/modals/profile.tsx +++ b/apps/web/src/components/modals/profile.tsx @@ -24,7 +24,7 @@ export default function Profile({ user }: { user: User }) { + )} + /> ) : ( <> - } + )} /> @@ -191,7 +197,7 @@ export function MobileNavbar() { navigate({ to: "/" }); 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" > @@ -202,7 +208,7 @@ export function MobileNavbar() { navigate({ to: "/settings" }); 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" > diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index c6fd3cc..10baf34 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -298,7 +298,7 @@ export default function Sidebar() { data-sidebar="sidebar" data-slot="sidebar" 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={{ transform: openMobile ? "translateX(0)" : "translateX(-100%)", opacity: openMobile ? 1 : 0, diff --git a/apps/web/src/features/conversation/list/body.tsx b/apps/web/src/features/conversation/list/body.tsx index cddee64..cc3baaf 100644 --- a/apps/web/src/features/conversation/list/body.tsx +++ b/apps/web/src/features/conversation/list/body.tsx @@ -31,7 +31,7 @@ export default function List() {
{items === null ? (
diff --git a/apps/web/src/routes/app/home.tsx b/apps/web/src/routes/app/home.tsx index c31a273..cd4f662 100644 --- a/apps/web/src/routes/app/home.tsx +++ b/apps/web/src/routes/app/home.tsx @@ -136,7 +136,7 @@ function AddConversationButton() { )} /> - + New Conversation diff --git a/packages/call/src/components/actions.tsx b/packages/call/src/components/actions.tsx index 7a6db77..6b0b2a3 100644 --- a/packages/call/src/components/actions.tsx +++ b/packages/call/src/components/actions.tsx @@ -31,7 +31,7 @@ import { import { useStorage } from "@tensamin/storage/context"; export default function Actions() { - const sharedClasses = "w-14 h-10"; + const sharedClasses = "w-14! h-10!"; const sharedIconSize = 15; const view = useCall((state) => state.view); const focusedParticipantId = useCall((state) => state.focusedParticipantId); @@ -83,13 +83,15 @@ export default function Actions() { {view === "focused" && ( ( - } + )} /> Hide users @@ -132,11 +134,15 @@ export default function Actions() { {isWatchingFocusedStream && focusedParticipantId !== ownId ? ( ( - } + )} /> Stop watching @@ -161,14 +167,18 @@ export default function Actions() { )} -
+
( - } + )} /> Popout @@ -184,13 +194,15 @@ export default function Actions() { ( - } + )} /> Fullscreen diff --git a/packages/call/src/components/buttons/deaf.tsx b/packages/call/src/components/buttons/deaf.tsx index 0de46a4..417e3f4 100644 --- a/packages/call/src/components/buttons/deaf.tsx +++ b/packages/call/src/components/buttons/deaf.tsx @@ -1,7 +1,11 @@ -import { Button, withTooltip } from "@methanium/ui"; +import { Button } from "@methanium/ui"; import { toggleDeaf, useCall } from "../../store"; import { HeadphoneOff, Headphones } from "lucide-react"; -import { type CallButtonProps, iconScale } from "./tooltipButton"; +import { + type CallButtonProps, + iconScale, + withButtonTooltip, +} from "./tooltipButton"; export default function DeafButton({ className, @@ -25,5 +29,5 @@ export default function DeafButton({ ); - return withTooltip(button, tooltip, portalContainer); + return withButtonTooltip(button, tooltip, portalContainer); } diff --git a/packages/call/src/components/buttons/invite.tsx b/packages/call/src/components/buttons/invite.tsx index 00ed70c..b478529 100644 --- a/packages/call/src/components/buttons/invite.tsx +++ b/packages/call/src/components/buttons/invite.tsx @@ -38,13 +38,13 @@ export default function InviteButton({ render={ tooltip ? ( + render={({ ref, onClick }) => ( + - } + )} /> ) : ( ); - return withTooltip(button, tooltip, portalContainer); + return withButtonTooltip(button, tooltip, portalContainer); } diff --git a/packages/call/src/components/buttons/mute.tsx b/packages/call/src/components/buttons/mute.tsx index 09c88b5..025f2ca 100644 --- a/packages/call/src/components/buttons/mute.tsx +++ b/packages/call/src/components/buttons/mute.tsx @@ -1,7 +1,11 @@ -import { Button, withTooltip } from "@methanium/ui"; +import { Button } from "@methanium/ui"; import { toggleMute, useCall } from "../../store"; import { Mic, MicOff } from "lucide-react"; -import { type CallButtonProps, iconScale } from "./tooltipButton"; +import { + type CallButtonProps, + iconScale, + withButtonTooltip, +} from "./tooltipButton"; export default function MuteButton({ className, @@ -25,5 +29,5 @@ export default function MuteButton({ ); - return withTooltip(button, tooltip, portalContainer); + return withButtonTooltip(button, tooltip, portalContainer); } diff --git a/packages/call/src/components/buttons/screenshare.tsx b/packages/call/src/components/buttons/screenshare.tsx index 00eddf3..4312dda 100644 --- a/packages/call/src/components/buttons/screenshare.tsx +++ b/packages/call/src/components/buttons/screenshare.tsx @@ -69,11 +69,16 @@ export default function ScreenshareButton({ tooltip ? ( ( - } + )} /> ) : ( - } + )} /> Click to open call page @@ -171,34 +186,23 @@ export function TinyPingGraph() { ); } -async function getPing(room: Room): Promise { - const report = await room.localParticipant - .getTrackPublication(Track.Source.Microphone) - ?.track?.getRTCStatsReport(); +export default function SidebarBox() { + const state = useCall((store) => store.state); + const isMobile = useIsMobile(); - if (!report) return; - - let bestRtt: number | undefined; - - report.forEach((stat) => { - if ( - stat.type === "candidate-pair" && - stat.state === "succeeded" && - stat.currentRoundTripTime != null - ) { - bestRtt = stat.currentRoundTripTime * 1000; - } - - if (stat.type === "remote-inbound-rtp" && stat.roundTripTime != null) { - bestRtt = stat.roundTripTime * 1000; - } - }); - - if (bestRtt == null || bestRtt <= 0) return; - - const roundedRtt = Math.round(bestRtt); - - if (roundedRtt <= 0) return; - - return roundedRtt; + return state === "closed" ? null : ( + + ); } diff --git a/packages/call/src/store.tsx b/packages/call/src/store.tsx index b5da895..883150c 100644 --- a/packages/call/src/store.tsx +++ b/packages/call/src/store.tsx @@ -1585,7 +1585,7 @@ export function useInitializeCall() { } setCurrentCallData({ - ...(data.data as z.infer), + ...mtp.CallData.response.parse(data.data), exists: true, }); }) diff --git a/packages/chat/src/components/input.tsx b/packages/chat/src/components/input.tsx index 13e39d3..2b64099 100644 --- a/packages/chat/src/components/input.tsx +++ b/packages/chat/src/components/input.tsx @@ -1,20 +1,16 @@ import Input from "@tensamin/markdown/input"; import { - Avatar, - AvatarFallback, - AvatarImage, Card, CardHeader, Popover, PopoverContent, PopoverTrigger, - Skeleton, } from "@methanium/ui"; import { useStorage } from "@tensamin/storage/context"; import React, { useEffect, useState, useRef } from "react"; 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 { useMTP } from "@tensamin/mtp"; import { log, toast } from "@tensamin/shared/log"; @@ -25,8 +21,7 @@ import { useSession } from "@tensamin/storage/session"; import GifPicker from "./gifPicker"; import EmojiPicker from "./emojiPicker"; import { useEmojiRanks, useRecordEmojiUse } from "./emojiRanks"; -import Wrapper from "@tensamin/user/wrapper"; -import Text from "@tensamin/markdown/text"; +import ReplyBox from "./replyBox"; export default function InputComponent({ value, @@ -196,54 +191,15 @@ export default function InputComponent({ return (
+ {/* reply */} {replyTo !== undefined && ( -
-
- - {replyMessage ? ( - <> - } - component={(user) => ( - - - - {user.Display.slice(2, 0).toUpperCase()} - - - )} - /> -
- -
- - ) : ( - <> - - - - )} - -
-
+ setReplyTo(undefined)} + userId={replyUserId ?? ownId} + variant="composer" + /> )}
-
@@ -280,15 +236,16 @@ export default function InputComponent({ onOpenChange={setEmojiPopoverOpen} > ( - } + )} /> + render={({ onClick }) => ( + - } + )} /> (null); + const [replyUser, setReplyUser] = useState(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 [editing, setEditing] = useState(false); const [editDraft, setEditDraft] = useState(message.Content); @@ -232,8 +272,16 @@ function MessageComponent({ return (
+ {/* reply */} + {replyMessage && replyUser && ( + + )} {user && message.Content ? ( -
- <> - {grouped ? ( -

- {new Date(message.SendTime).toLocaleString([], { - hour: "2-digit", - minute: "2-digit", - })} -

- ) : ( - - - - {user.Display.slice(0, 2).toUpperCase()} - - + <> +
- {!grouped && ( -
-

{user.Display}

-

- {new Date(message.SendTime).toLocaleString([], { - hour: "2-digit", - minute: "2-digit", - })} -

-
- {message.MessageState === "read" ? ( - - ) : message.MessageState === "received" ? ( - - ) : message.MessageState === "sent" ? ( - - ) : message.MessageState === "sending" ? ( - - ) : null} - {actuallyFailed && ( - + > + <> + {grouped ? ( +

+ {new Date(message.SendTime).toLocaleString([], { + hour: "2-digit", + minute: "2-digit", + })} +

+ ) : ( + + + + {user.Display.slice(0, 2).toUpperCase()} + + + )} +
+ {!grouped && ( +
+

{user.Display}

+

+ {new Date(message.SendTime).toLocaleString([], { + hour: "2-digit", + minute: "2-digit", + })} +

+
+ {message.MessageState === "read" ? ( + + ) : message.MessageState === "received" ? ( + + ) : message.MessageState === "sent" ? ( + + ) : message.MessageState === "sending" ? ( + + ) : null} + {actuallyFailed && ( + + )} +
+
+ )} + {editing ? ( +
+ { + submitEditMessage(editDraft); + setEditing(false); + }} + /> +
+ + +
+
+ ) : isValidURL ? ( + + ) : ( + + )} + {groupedReactions.length > 0 && ( +
+ {groupedReactions.map( + ([reaction, { count, reactedByMe }]) => ( + + ), )}
-
- )} - {editing ? ( -
- { - submitEditMessage(editDraft); - setEditing(false); - }} - /> -
- - -
-
- ) : isValidURL ? ( - - ) : ( - - )} - {groupedReactions.length > 0 && ( -
- {groupedReactions.map( - ([reaction, { count, reactedByMe }]) => ( - - ), - )} -
- )} -
- -
+ )} +
+ +
+
) : ( @@ -388,6 +446,7 @@ export default memo(MessageComponent, (prev, next) => { prev.message.SendTime === next.message.SendTime && prev.message.Content === next.message.Content && prev.message.SenderId === next.message.SenderId && + prev.message.ReplyId === next.message.ReplyId && prev.message.MessageState === next.message.MessageState && prev.message.failed === next.message.failed && prev.message.decryptionFailed === next.message.decryptionFailed && diff --git a/packages/chat/src/components/messageContextMenu.tsx b/packages/chat/src/components/messageContextMenu.tsx index d61efcf..dec94b5 100644 --- a/packages/chat/src/components/messageContextMenu.tsx +++ b/packages/chat/src/components/messageContextMenu.tsx @@ -35,11 +35,13 @@ import { Reply, Trash, } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; import { cloneElement, useMemo, useState, useSyncExternalStore } from "react"; import type { MouseEvent as ReactMouseEvent, ReactElement, ReactNode, + Ref, } from "react"; import { useChat } from "../context"; import Emoji from "@tensamin/markdown/emoji"; @@ -260,18 +262,32 @@ function MiniMenuTooltip({ children, label, }: { - children: ReactElement; + children: ReactElement<{ + onClick?: (event: ReactMouseEvent) => void; + ref?: Ref; + }>; label: string; }) { return ( - + + cloneElement(children, { + ref, + onClick: (event) => { + onClick?.(event); + children.props.onClick?.(event); + }, + }) + } + /> {label} ); } function MiniMessageMenu({ + fadeOut, isOwnMessage, onDelete, onEdit, @@ -283,6 +299,7 @@ function MiniMessageMenu({ onReply, shiftIsPressed, }: { + fadeOut: boolean; isOwnMessage: boolean; onDelete: () => void; onEdit: () => void; @@ -295,8 +312,12 @@ function MiniMessageMenu({ shiftIsPressed: boolean; }) { return ( -
- + + {quickReactions.map((emoji) => ( ( - } + )} /> ) : (
+ ); } @@ -538,8 +560,11 @@ export default function MessageContextMenu({ [], ); const [mainDrawerOpen, setMainDrawerOpen] = useState(false); + const [contextMenuOpen, setContextMenuOpen] = useState(false); const [reactionDrawerOpen, setReactionDrawerOpen] = useState(false); const [pickerOpen, setPickerOpen] = useState(false); + const [pickerClosing, setPickerClosing] = useState(false); + const [fadeMiniMenu, setFadeMiniMenu] = useState(false); const { ranks } = useEmojiRanks(); const quickReactions = getRecentEmojis(ranks, 3); const menuReactions = getRecentEmojis(ranks, 5); @@ -547,7 +572,11 @@ export default function MessageContextMenu({ function selectReaction(emoji: string) { void onReact(emoji); setReactionDrawerOpen(false); - setPickerOpen(false); + setPickerVisibility(false); + } + function setPickerVisibility(open: boolean) { + setPickerOpen(open); + setPickerClosing(!isMobile && !open); } const mainDrawerComponents = useMemo( () => @@ -578,20 +607,28 @@ export default function MessageContextMenu({ onPointerLeave={scheduleMiniMenuClose} > {children} - {!isMobile && !hideMiniMenu && activeMenuId === messageId && ( - deleteMessage(messageId)} - onEdit={() => onSetEditing(true)} - onOpenMenu={openMenu} - onOpenPicker={() => setPickerOpen(true)} - usePickerTrigger={!isMobile} - onReact={selectReaction} - quickReactions={quickReactions} - onReply={() => setReplyTo(messageId)} - shiftIsPressed={shiftIsPressed} - /> - )} + setFadeMiniMenu(false)}> + {!isMobile && + !hideMiniMenu && + (activeMenuId === messageId || + contextMenuOpen || + pickerOpen || + pickerClosing) && ( + deleteMessage(messageId)} + onEdit={() => onSetEditing(true)} + onOpenMenu={openMenu} + onOpenPicker={() => setPickerVisibility(true)} + usePickerTrigger={!isMobile} + onReact={selectReaction} + quickReactions={quickReactions} + onReply={() => setReplyTo(messageId)} + shiftIsPressed={shiftIsPressed} + /> + )} +
); } @@ -636,14 +673,14 @@ export default function MessageContextMenu({ emojis={menuReactions} onMore={() => { setReactionDrawerOpen(false); - setPickerOpen(true); + setPickerVisibility(true); }} onSelect={selectReaction} />
- + Choose an emoji @@ -672,8 +709,17 @@ export default function MessageContextMenu({ }); return ( - - + { + setPickerVisibility(open); + setFadeMiniMenu(!open && eventDetails.reason === "outside-press"); + }} + onOpenChangeComplete={(open) => { + if (!open) setPickerClosing(false); + }} + > + setPickerOpen(true)} + onAddReaction={() => setPickerVisibility(true)} onReact={selectReaction} reactionEmojis={menuReactions} onSetEditing={onSetEditing} diff --git a/packages/chat/src/components/replyBox.tsx b/packages/chat/src/components/replyBox.tsx new file mode 100644 index 0000000..b171457 --- /dev/null +++ b/packages/chat/src/components/replyBox.tsx @@ -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 ( +
+ + + + {user.Display.slice(0, 2).toUpperCase()} + + +

+ {user.Display} +

+
+ ); +} + +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 ( +
+
+ + {loading ? ( + <> + + + + ) : ( + <> + {user ? ( + + ) : userId ? ( + } + component={(resolvedUser) => } + /> + ) : null} + {content !== undefined && ( +
+ +
+ )} + + )} + {onDismiss && ( + + )} +
+
+ ); +} diff --git a/packages/chat/src/context.tsx b/packages/chat/src/context.tsx index c69cd9c..ecd3173 100644 --- a/packages/chat/src/context.tsx +++ b/packages/chat/src/context.tsx @@ -119,8 +119,45 @@ type SendMessageGet = ( type GetChatSecret = (userId: number) => Promise; +export async function getMessage({ + sendTime, + ownId, + chatPartnerId, + send, +}: { + sendTime: number; + ownId: number; + chatPartnerId: number; + send: SendMessageGet; +}): Promise { + 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({ replyTo, + ownId, + chatUserId, send, getChatSecret, }: { @@ -130,22 +167,22 @@ export async function fetchReplyMessage({ send: SendMessageGet; getChatSecret: GetChatSecret; }) { - const value = await send("MessageGet", { - SendTime: replyTo, + const message = await getMessage({ + sendTime: replyTo, + ownId, + chatPartnerId: chatUserId, + send, }); - const chatSecret = await getChatSecret(value.data.SenderId); + const chatSecret = await getChatSecret(chatUserId); if (!chatSecret) { throw new Error("Missing chat secret"); } - const decryptedContent = await decryptChatText( - chatSecret, - value.data.Content, - ); + const decryptedContent = await decryptChatText(chatSecret, message.Content); return { - ...value.data, + ...message, Content: decryptedContent, }; } @@ -791,6 +828,9 @@ export default function Provider({ children }: { children: ReactNode }) { // Replys const [replyTo, setReplyTo] = useState(undefined); + useEffect(() => { + setReplyTo(undefined); + }, [userIdValue]); return ( diff --git a/packages/chat/src/screen.tsx b/packages/chat/src/screen.tsx index e8c3928..bf3a1cf 100644 --- a/packages/chat/src/screen.tsx +++ b/packages/chat/src/screen.tsx @@ -478,6 +478,7 @@ export default function Screen() { const lastMessage = messages[messageIndex - 1]; const isGrouped = lastMessage && + !message.ReplyId && lastMessage.SenderId === message.SenderId && Math.round(lastMessage.SendTime / 10000) === Math.round(message.SendTime / 10000); diff --git a/packages/chat/todo.md b/packages/chat/todo.md index 2ce0e5f..34f37ce 100644 --- a/packages/chat/todo.md +++ b/packages/chat/todo.md @@ -8,5 +8,7 @@ - Confirmation when exiting with text in the input box. - Add arrow up hotkey to edit last message (req: packages/hotkeys) - Drop any unique reactions above 10 -- Make the emoji picker not get moved with the mini context menu -- Add proper loading skeleton +- Add proper loading skeleton for individual messages +- Sounds +- Reply jumping +- Add emoji picker diff --git a/packages/markdown/src/text.tsx b/packages/markdown/src/text.tsx index 6b159a2..ca65fe0 100644 --- a/packages/markdown/src/text.tsx +++ b/packages/markdown/src/text.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useMemo, type CSSProperties } from "react"; import { ensureMarkdownStyles, @@ -8,6 +8,7 @@ import { export type TextProps = { value: string; + fontSize?: CSSProperties["fontSize"]; }; /** @@ -21,5 +22,9 @@ export default function Text(props: TextProps) { const blocks = useMemo(() => parseMarkdownBlocks(props.value), [props.value]); const renderedBlocks = useMemo(() => renderBlocks(blocks), [blocks]); - return
{renderedBlocks}
; + return ( +
+ {renderedBlocks} +
+ ); } diff --git a/packages/settings/src/layout.tsx b/packages/settings/src/layout.tsx index 37e0ef2..095fd15 100644 --- a/packages/settings/src/layout.tsx +++ b/packages/settings/src/layout.tsx @@ -1,5 +1,6 @@ import { Outlet, useLocation, useNavigate } from "@tanstack/react-router"; import { Button, ClearStorageButton, cn, useIsMobile } from "@methanium/ui"; +import { ArrowLeft } from "lucide-react"; import { settingsNavigation } from "./manifest"; @@ -8,24 +9,41 @@ export default function SettingsLayout() { const location = useLocation(); return ( -
- {!isMobile && } -
-

- {location.pathname - .split("/") - .pop() - ?.replace(/-/g, " ") - .replace(/\b\w/g, (letter) => letter.toUpperCase())} -

+
+ +
+
+ {isMobile && !/^\/settings\/?$/.test(location.pathname) && ( + + )} +

+ {location.pathname + .split("/") + .pop() + ?.replace(/-/g, " ") + .replace(/\b\w/g, (letter) => letter.toUpperCase())} +

+
); } -export function SettingsSidebar() { - const isMobile = useIsMobile(); +export function SettingsSidebar({ + mobile = false, + className, +}: { + mobile?: boolean; + className?: string; +}) { const navigate = useNavigate(); const categories = [ ...new Set(settingsNavigation.map((page) => page.category)), @@ -34,10 +52,11 @@ export function SettingsSidebar() { return (
{categories.map((category) => ( diff --git a/packages/settings/src/manifest.ts b/packages/settings/src/manifest.ts index 38a404f..3002928 100644 --- a/packages/settings/src/manifest.ts +++ b/packages/settings/src/manifest.ts @@ -8,7 +8,6 @@ import Theme from "./pages/theme"; export const settingsPages = [ { path: "/", component: Index }, - { category: "general", path: "chat", label: "Chat", component: Chat }, { category: "account", path: "profile", @@ -21,6 +20,7 @@ export const settingsPages = [ label: "Security", component: Security, }, + { category: "general", path: "chat", label: "Chat", component: Chat }, { category: "application", path: "cache", label: "Cache", component: Cache }, { category: "application", path: "theme", label: "Theme", component: Theme }, { diff --git a/packages/settings/src/pages/index.tsx b/packages/settings/src/pages/index.tsx index b045749..803a0c1 100644 --- a/packages/settings/src/pages/index.tsx +++ b/packages/settings/src/pages/index.tsx @@ -1,7 +1,9 @@ -import { useIsMobile } from "@methanium/ui"; import { SettingsSidebar } from "../layout"; export default function Page() { - const isMobile = useIsMobile(); - return isMobile && ; + return ( +
+ +
+ ); } diff --git a/packages/settings/src/pages/licenses.tsx b/packages/settings/src/pages/licenses.tsx index 2e34445..ebf3578 100644 --- a/packages/settings/src/pages/licenses.tsx +++ b/packages/settings/src/pages/licenses.tsx @@ -40,7 +40,7 @@ export default function Page() {

Package Count: {packageCount}

-
+
{packages.map((licensePackage) => ( ; +const USER_CACHE_MAX_AGE = 5 * 60 * 1000; + interface contextValue { get(userId: number): Promise; } @@ -31,6 +33,7 @@ const UserContext = createContext(undefined); export default function UserProvider(props: { children: ReactNode }) { const storageRef = useRef>({}); const pendingRef = useRef | undefined>>({}); + const checkedAtRef = useRef>({}); const { send } = useMTP(); const { load } = useStorage(); @@ -66,17 +69,36 @@ export default function UserProvider(props: { children: ReactNode }) { const cachedResult = schemas.GetUserData.response.safeParse(cachedValue); 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 { 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")) { throw new Error(`GetUserData failed: ${userData.type}`); } const user = userData.data; storageRef.current[userId] = user; + checkedAtRef.current[userId] = Date.now(); return user; } catch (error) { - if (cached) return cached; + if (cached && storageRef.current[userId]) { + checkedAtRef.current[userId] = Date.now(); + return cached; + } throw error; } })(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c058945..ab3dd0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false 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 importers: @@ -13,8 +13,8 @@ importers: .: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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: 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 @@ -89,8 +89,8 @@ importers: apps/tauri: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: ^2 version: 2.11.1 @@ -153,8 +153,8 @@ importers: specifier: ^5.2.7 version: 5.2.7 '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: ^1.1.0 version: 1.1.4 @@ -532,8 +532,8 @@ importers: 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) '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: ^1.169.1 version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -583,8 +583,8 @@ importers: packages/chat: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: ^0.21.1 version: 0.21.1 @@ -664,8 +664,8 @@ importers: specifier: ^6.41.1 version: 6.43.4 '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: ^17.0.3 version: 17.0.3 @@ -682,8 +682,8 @@ importers: packages/mtp: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: ^2 version: 2.11.1 @@ -722,8 +722,8 @@ importers: packages/notifications: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: ^1.169.1 version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -764,8 +764,8 @@ importers: packages/onboarding: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: workspace:* version: link:../shared @@ -788,8 +788,8 @@ importers: packages/settings: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: ^1.169.1 version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -828,8 +828,8 @@ importers: packages/shared: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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: specifier: ^1.14.0 version: 1.23.0(react@19.2.7) @@ -846,8 +846,8 @@ importers: packages/storage: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: ^2 version: 2.11.1 @@ -870,8 +870,8 @@ importers: packages/tauth: dependencies: '@methanium/ui': - specifier: https://git.methanium.net/methanium/ui/releases/download/0.0.10/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) + 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.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': specifier: ^1.0.0 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': 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': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -1550,9 +1547,9 @@ packages: '@marijn/find-cluster-break@1.0.3': resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} - '@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.10/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} - version: 0.0.10 + '@methanium/ui@https://git.methanium.net/methanium/ui/releases/download/0.0.22/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.22 peerDependencies: react: ^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.3.0': {} - '@hono/node-server@1.19.14(hono@4.12.27)': dependencies: hono: 4.12.27 @@ -5865,10 +5860,9 @@ snapshots: '@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: '@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 class-variance-authority: 0.7.1 clsx: 2.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d8793c9..87d0c28 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,5 +6,5 @@ allowBuilds: electron-winstaller: true esbuild: true 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"