diff --git a/deno.lock b/deno.lock index ddd91281..061fb4a5 100644 --- a/deno.lock +++ b/deno.lock @@ -6468,6 +6468,7 @@ "npm:@radix-ui/react-scroll-area@^1.2.3", "npm:@radix-ui/react-select@^2.1.6", "npm:@radix-ui/react-separator@^1.1.2", + "npm:@radix-ui/react-slider@^1.3.2", "npm:@radix-ui/react-switch@^1.1.3", "npm:@radix-ui/react-tabs@^1.1.3", "npm:@radix-ui/react-toast@^1.2.6", diff --git a/public/Logo.svg b/public/Logo.svg new file mode 100644 index 00000000..e6863f6a --- /dev/null +++ b/public/Logo.svg @@ -0,0 +1,16 @@ + + + +Created with Fabric.js 4.6.0 + + + + + + + + + + + + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 35152fd1..4f9a5a2d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,5 @@ import { DeviceWrapper } from "@app/DeviceWrapper.tsx"; import { PageRouter } from "@app/PageRouter.tsx"; -import { DeviceSelector } from "@components/DeviceSelector.tsx"; import { DialogManager } from "@components/Dialog/DialogManager.tsx"; import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.tsx"; import { KeyBackupReminder } from "@components/KeyBackupReminder.tsx"; @@ -14,6 +13,7 @@ import { ErrorBoundary } from "react-error-boundary"; import { ErrorPage } from "@components/UI/ErrorPage.tsx"; import { MapProvider } from "react-map-gl/maplibre"; import { CommandPalette } from "@components/CommandPalette/index.tsx"; +import { SidebarProvider } from "@core/stores/sidebarStore.tsx"; export const App = (): JSX.Element => { @@ -23,6 +23,7 @@ export const App = (): JSX.Element => { const device = getDevice(selectedDevice); + return ( { /> -
-
- -
+
+ +
{device ? ( -
+
@@ -53,9 +53,9 @@ export const App = (): JSX.Element => { )}
-
+
- + ); }; diff --git a/src/components/CommandPalette/index.tsx b/src/components/CommandPalette/index.tsx index 70699d81..11612984 100644 --- a/src/components/CommandPalette/index.tsx +++ b/src/components/CommandPalette/index.tsx @@ -60,7 +60,7 @@ export const CommandPalette = () => { setSelectedDevice, } = useAppStore(); const { getDevices } = useDeviceStore(); - const { setDialogOpen, setActivePage, connection } = useDevice(); + const { setDialogOpen, setActivePage, getNode, connection } = useDevice(); const { pinnedItems, togglePinnedItem } = usePinnedItems({ storageName: 'pinnedCommandMenuGroups' }); const groups: Group[] = [ @@ -115,12 +115,12 @@ export const CommandPalette = () => { icon: ArrowLeftRightIcon, subItems: getDevices().map((device) => ({ label: - device.nodes.get(device.hardware.myNodeNum)?.user?.longName ?? + getNode(device.hardware.myNodeNum)?.user?.longName ?? device.hardware.myNodeNum.toString(), icon: ( @@ -222,7 +222,7 @@ export const CommandPalette = () => { label: "Clear All Stored Message", icon: EraserIcon, action() { - setDialogOpen("clearMessages", true); + setDialogOpen("deleteMessages", true); }, }, ], @@ -262,7 +262,7 @@ export const CommandPalette = () => { type="button" onClick={() => togglePinnedItem(group.label)} className={cn( - "transition-all duration-300 scale-100 cursor-pointer m-0.5 p-2 focus:*:data-label:opacity-100" + "transition-all duration-300 scale-100 cursor-pointer p-2 focus:*:data-label:opacity-100" )} aria-description={ pinnedItems.includes(group.label) diff --git a/src/components/DeviceSelector.tsx b/src/components/DeviceSelector.tsx deleted file mode 100644 index 872b9c04..00000000 --- a/src/components/DeviceSelector.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { DeviceSelectorButton } from "@components/DeviceSelectorButton.tsx"; -import ThemeSwitcher from "@components/ThemeSwitcher.tsx"; -import { Separator } from "@components/UI/Seperator.tsx"; -import { Code } from "@components/UI/Typography/Code.tsx"; -import { useAppStore } from "@core/stores/appStore.ts"; -import { useDeviceStore } from "@core/stores/deviceStore.ts"; -import { HomeIcon, PlusIcon, SearchIcon } from "lucide-react"; -import { Avatar } from "@components/UI/Avatar.tsx"; - -export const DeviceSelector = () => { - const { getDevices } = useDeviceStore(); - const { - selectedDevice, - setSelectedDevice, - setCommandPaletteOpen, - setConnectDialogOpen, - } = useAppStore(); - - return ( - - ); -}; diff --git a/src/components/DeviceSelectorButton.tsx b/src/components/DeviceSelectorButton.tsx deleted file mode 100644 index 62c2bc2a..00000000 --- a/src/components/DeviceSelectorButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export interface DeviceSelectorButtonProps { - active: boolean; - onClick: () => void; - children?: React.ReactNode; -} - -export const DeviceSelectorButton = ({ - onClick, - children, -}: DeviceSelectorButtonProps) => ( -
  • - { - /* {active && ( -
    - )} */ - } -
    - {children} -
    -
  • -); diff --git a/src/components/Dialog/DeviceNameDialog.tsx b/src/components/Dialog/DeviceNameDialog.tsx index 765e4097..26abf6cd 100644 --- a/src/components/Dialog/DeviceNameDialog.tsx +++ b/src/components/Dialog/DeviceNameDialog.tsx @@ -10,10 +10,11 @@ import { DialogHeader, DialogTitle, } from "@components/UI/Dialog.tsx"; -import { Input } from "@components/UI/Input.tsx"; import { Label } from "@components/UI/Label.tsx"; import { Protobuf } from "@meshtastic/core"; import { useForm } from "react-hook-form"; +import { GenericInput } from "@components/Form/FormInput.tsx"; +import { validateMaxByteLength } from "@core/utils/string.ts"; export interface User { longName: string; @@ -24,32 +25,44 @@ export interface DeviceNameDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } +const MAX_LONG_NAME_BYTE_LENGTH = 40; +const MAX_SHORT_NAME_BYTE_LENGTH = 4; export const DeviceNameDialog = ({ open, onOpenChange, }: DeviceNameDialogProps) => { - const { hardware, nodes, connection } = useDevice(); + const { hardware, getNode, connection } = useDevice(); + const myNode = getNode(hardware.myNodeNum); - const myNode = nodes.get(hardware.myNodeNum); + const defaultValues = { + longName: myNode?.user?.longName ?? "Unknown", + shortName: myNode?.user?.shortName ?? "??", + }; - const { register, handleSubmit } = useForm({ - values: { - longName: myNode?.user?.longName ?? "Unknown", - shortName: myNode?.user?.shortName ?? "Unknown", - }, + const { getValues, setValue, reset, control, handleSubmit } = useForm({ + values: defaultValues, }); + const { currentLength: currentLongNameLength } = validateMaxByteLength(getValues('longName'), MAX_LONG_NAME_BYTE_LENGTH); + const { currentLength: currentShortNameLength } = validateMaxByteLength(getValues('shortName'), MAX_SHORT_NAME_BYTE_LENGTH); + const onSubmit = handleSubmit((data) => { connection?.setOwner( create(Protobuf.Mesh.UserSchema, { - ...myNode?.user, + ...(myNode?.user ?? {}), ...data, }), ); onOpenChange(false); }); + const handleReset = () => { + reset({ longName: "", shortName: "" }); + setValue("longName", ""); + setValue("shortName", ""); + }; + return ( @@ -60,22 +73,50 @@ export const DeviceNameDialog = ({ The Device will restart once the config is saved. -
    -
    - - - - +
    + + - -
    - - - +
    +
    + + +
    + + + + + +
    ); -}; +}; \ No newline at end of file diff --git a/src/components/Dialog/LocationResponseDialog.tsx b/src/components/Dialog/LocationResponseDialog.tsx index c4df3761..4625fa87 100644 --- a/src/components/Dialog/LocationResponseDialog.tsx +++ b/src/components/Dialog/LocationResponseDialog.tsx @@ -21,9 +21,9 @@ export const LocationResponseDialog = ({ open, onOpenChange, }: LocationResponseDialogProps) => { - const { nodes } = useDevice(); + const { getNode } = useDevice(); - const from = nodes.get(location?.from ?? 0); + const from = getNode(location?.from ?? 0); const longName = from?.user?.longName ?? (from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown"); const shortName = from?.user?.shortName ?? diff --git a/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx b/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx index f010fc67..a667363f 100644 --- a/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx +++ b/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx @@ -29,10 +29,10 @@ export const NodeDetailsDialog = ({ open, onOpenChange, }: NodeDetailsDialogProps) => { - const { nodes } = useDevice(); + const { getNode } = useDevice(); const { nodeNumDetails } = useAppStore(); - const device = nodes.get(nodeNumDetails); + const device = getNode(nodeNumDetails); if (!device) return null; diff --git a/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx b/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx index ad1373f4..e39c446b 100644 --- a/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx +++ b/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx @@ -18,17 +18,16 @@ export interface RefreshKeysDialogProps { export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => { const { activeChat } = useMessageStore(); - const { nodeErrors, nodes } = useDevice(); + const { nodeErrors, getNode } = useDevice(); const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog(); const nodeErrorNum = nodeErrors.get(activeChat); if (!nodeErrorNum) { - console.error("Node with error not found"); return null; } - const nodeWithError = nodes.get(nodeErrorNum?.node ?? 0); + const nodeWithError = getNode(nodeErrorNum?.node ?? 0); if (!nodeWithError) { console.error("Node with error not found"); diff --git a/src/components/Dialog/RemoveNodeDialog.tsx b/src/components/Dialog/RemoveNodeDialog.tsx index 66297f23..66c42dc6 100644 --- a/src/components/Dialog/RemoveNodeDialog.tsx +++ b/src/components/Dialog/RemoveNodeDialog.tsx @@ -21,7 +21,7 @@ export const RemoveNodeDialog = ({ open, onOpenChange, }: RemoveNodeDialogProps) => { - const { connection, nodes, removeNode } = useDevice(); + const { connection, getNode, removeNode } = useDevice(); const { nodeNumToBeRemoved } = useAppStore(); const onSubmit = () => { @@ -42,7 +42,7 @@ export const RemoveNodeDialog = ({
    - +
    diff --git a/src/components/Dialog/TracerouteResponseDialog.tsx b/src/components/Dialog/TracerouteResponseDialog.tsx index 9f5198f6..3ce35765 100644 --- a/src/components/Dialog/TracerouteResponseDialog.tsx +++ b/src/components/Dialog/TracerouteResponseDialog.tsx @@ -23,17 +23,17 @@ export const TracerouteResponseDialog = ({ open, onOpenChange, }: TracerouteResponseDialogProps) => { - const { nodes } = useDevice(); + const { getNode } = useDevice(); const route: number[] = traceroute?.data.route ?? []; const routeBack: number[] = traceroute?.data.routeBack ?? []; const snrTowards = (traceroute?.data.snrTowards ?? []).map(snr => snr / 4); const snrBack = (traceroute?.data.snrBack ?? []).map(snr => snr / 4); - const from = nodes.get(traceroute?.from ?? 0); + const from = getNode(traceroute?.from ?? 0); const longName = from?.user?.longName ?? (from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown"); const shortName = from?.user?.shortName ?? (from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK"); - const to = nodes.get(traceroute?.to ?? 0); + const to = getNode(traceroute?.to ?? 0); return ( diff --git a/src/components/PageComponents/Config/Security/Security.tsx b/src/components/PageComponents/Config/Security/Security.tsx index 21613a3f..6c6b880a 100644 --- a/src/components/PageComponents/Config/Security/Security.tsx +++ b/src/components/PageComponents/Config/Security/Security.tsx @@ -270,11 +270,6 @@ export const Security = () => { properties: { value: state.adminKey, showCopyButton: true, - action: { - icon: state.adminKeyVisible ? EyeOff : Eye, - onClick: () => - dispatch({ type: "TOGGLE_ADMIN_KEY_VISIBILITY" }), - }, }, }, ], diff --git a/src/components/PageComponents/Map/NodeDetail.tsx b/src/components/PageComponents/Map/NodeDetail.tsx index 9c46e642..73e75e20 100644 --- a/src/components/PageComponents/Map/NodeDetail.tsx +++ b/src/components/PageComponents/Map/NodeDetail.tsx @@ -7,7 +7,6 @@ import { Mono } from "@components/generic/Mono.tsx"; import { TimeAgo } from "@components/generic/TimeAgo.tsx"; import { Protobuf } from "@meshtastic/core"; import type { Protobuf as ProtobufType } from "@meshtastic/core"; -import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { BatteryChargingIcon, BatteryFullIcon, @@ -37,7 +36,7 @@ export interface NodeDetailProps { export const NodeDetail = ({ node }: NodeDetailProps) => { const { setChatType, setActiveChat } = useMessageStore(); const { setActivePage } = useDevice(); - const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`; + const name = node.user?.longName ?? `UNK`; const shortName = node.user?.shortName ?? "UNK"; const hwModel = node.user?.hwModel ?? 0; const hardwareType = @@ -53,7 +52,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
    - +
    { // Required to prevent DM tooltip auto-appearing on creation diff --git a/src/components/PageComponents/Messages/ChannelChat.tsx b/src/components/PageComponents/Messages/ChannelChat.tsx index 26cd6720..97f2973a 100644 --- a/src/components/PageComponents/Messages/ChannelChat.tsx +++ b/src/components/PageComponents/Messages/ChannelChat.tsx @@ -8,7 +8,7 @@ export interface ChannelChatProps { } const EmptyState = () => ( -
    +
    No Messages
    @@ -18,55 +18,53 @@ export const ChannelChat = ({ messages = [], }: ChannelChatProps) => { const messagesEndRef = useRef(null); - const scrollContainerRef = useRef(null); + const scrollContainerRef = useRef(null); - const scrollToBottom = useCallback(() => { + const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => { const scrollContainer = scrollContainerRef.current; - if (scrollContainer) { - const isNearBottom = - scrollContainer.scrollHeight - - scrollContainer.scrollTop - - scrollContainer.clientHeight < - 100; + if (!scrollContainer) return; - if (isNearBottom) { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - } + const isNearBottom = + scrollContainer.scrollHeight - + scrollContainer.scrollTop - + scrollContainer.clientHeight < 100; // Threshold in pixels + + if (isNearBottom || behavior === 'instant') { + messagesEndRef.current?.scrollIntoView({ behavior }); } }, []); useEffect(() => { - scrollToBottom(); - }, [scrollToBottom, messages]); + scrollToBottom('smooth'); + }, [messages, scrollToBottom]); + + useEffect(() => { + scrollToBottom('instant'); + }, [scrollToBottom]); if (!messages?.length) { return ( -
    -
    - -
    -
    +
      + + +
    ); } return ( -
    -
    -
    - {messages?.map((message) => { - return ( - - ); - })} -
    -
    -
    -
    +
      + {messages?.map((message) => { + return ( + + ); + })} +
      +
    ); }; \ No newline at end of file diff --git a/src/components/PageComponents/Messages/MessageActionsMenu.tsx b/src/components/PageComponents/Messages/MessageActionsMenu.tsx index d0843f32..9f575798 100644 --- a/src/components/PageComponents/Messages/MessageActionsMenu.tsx +++ b/src/components/PageComponents/Messages/MessageActionsMenu.tsx @@ -18,7 +18,7 @@ export const MessageActionsMenu = ({ onReply }: MessageActionsMenuProps) => { const hoverIconBarClass = cn( - "absolute top-2 right-4", + "absolute top-2 right-2", "flex items-center gap-x-1", "bg-white dark:bg-zinc-800", "border border-gray-200 dark:border-zinc-600", diff --git a/src/components/PageComponents/Messages/MessageInput.tsx b/src/components/PageComponents/Messages/MessageInput.tsx index a2383d0a..f64dbc91 100644 --- a/src/components/PageComponents/Messages/MessageInput.tsx +++ b/src/components/PageComponents/Messages/MessageInput.tsx @@ -75,8 +75,8 @@ export const MessageInput = ({ return (
    -
    -
    + +
    -