Browse Source

feat: replace HashIcon due to incompatibility with React 19

pull/380/head
Dan Ditomaso 1 year ago
parent
commit
8fa2cb0b19
  1. 36
      src/components/CommandPalette.tsx
  2. 8
      src/components/DeviceSelector.tsx
  3. 6
      src/components/DeviceSelectorButton.tsx
  4. 9
      src/components/PageComponents/Map/NodeDetail.tsx
  5. 11
      src/components/PageComponents/Messages/Message.tsx
  6. 99
      src/components/UI/Avatar.tsx
  7. 8
      src/components/UI/Sidebar/SidebarSection.tsx
  8. 6
      src/components/UI/Sidebar/sidebarButton.tsx
  9. 19
      src/pages/Map.tsx
  10. 13
      src/pages/Messages.tsx
  11. 12
      src/pages/Nodes.tsx

36
src/components/CommandPalette.tsx

@ -8,7 +8,6 @@ import {
} from "@components/UI/Command.tsx"; } from "@components/UI/Command.tsx";
import { useAppStore } from "@core/stores/appStore.ts"; import { useAppStore } from "@core/stores/appStore.ts";
import { useDevice, useDeviceStore } from "@core/stores/deviceStore.ts"; import { useDevice, useDeviceStore } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { useCommandState } from "cmdk"; import { useCommandState } from "cmdk";
import { import {
ArrowLeftRightIcon, ArrowLeftRightIcon,
@ -35,6 +34,7 @@ import {
XCircleIcon, XCircleIcon,
} from "lucide-react"; } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { Avatar } from "./UI/Avatar";
export interface Group { export interface Group {
label: string; label: string;
@ -120,15 +120,16 @@ export const CommandPalette = (): JSX.Element => {
label: "Switch Node", label: "Switch Node",
icon: ArrowLeftRightIcon, icon: ArrowLeftRightIcon,
subItems: getDevices().map((device) => { subItems: getDevices().map((device) => {
const node = device.nodes.get(device.hardware.myNodeNum);
const longName = node?.user?.longName;
const shortName = node?.user?.shortName.toString();
const nodeNum = device.hardware.myNodeNum.toString();
return { return {
label: label:
device.nodes.get(device.hardware.myNodeNum)?.user?.longName ?? longName ?? nodeNum,
device.hardware.myNodeNum.toString(),
icon: ( icon: (
<Hashicon <Avatar text={shortName?.toString() ?? nodeNum} />
size={16}
value={device.hardware.myNodeNum.toString()}
/>
), ),
action() { action() {
setSelectedDevice(device.id); setSelectedDevice(device.id);
@ -254,8 +255,7 @@ export const CommandPalette = (): JSX.Element => {
label: "Red", label: "Red",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${darkMode ? "bg-[#f25555]" : "bg-[#f28585]"
darkMode ? "bg-[#f25555]" : "bg-[#f28585]"
}`} }`}
/> />
), ),
@ -267,8 +267,7 @@ export const CommandPalette = (): JSX.Element => {
label: "Orange", label: "Orange",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${darkMode ? "bg-[#e1720b]" : "bg-[#edb17a]"
darkMode ? "bg-[#e1720b]" : "bg-[#edb17a]"
}`} }`}
/> />
), ),
@ -280,8 +279,7 @@ export const CommandPalette = (): JSX.Element => {
label: "Yellow", label: "Yellow",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${darkMode ? "bg-[#ac8c1a]" : "bg-[#e0cc87]"
darkMode ? "bg-[#ac8c1a]" : "bg-[#e0cc87]"
}`} }`}
/> />
), ),
@ -293,8 +291,7 @@ export const CommandPalette = (): JSX.Element => {
label: "Green", label: "Green",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${darkMode ? "bg-[#27a341]" : "bg-[#8bc9c5]"
darkMode ? "bg-[#27a341]" : "bg-[#8bc9c5]"
}`} }`}
/> />
), ),
@ -306,8 +303,7 @@ export const CommandPalette = (): JSX.Element => {
label: "Blue", label: "Blue",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${darkMode ? "bg-[#2093fe]" : "bg-[#70afea]"
darkMode ? "bg-[#2093fe]" : "bg-[#70afea]"
}`} }`}
/> />
), ),
@ -319,8 +315,7 @@ export const CommandPalette = (): JSX.Element => {
label: "Purple", label: "Purple",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${darkMode ? "bg-[#926bff]" : "bg-[#a09eef]"
darkMode ? "bg-[#926bff]" : "bg-[#a09eef]"
}`} }`}
/> />
), ),
@ -332,8 +327,7 @@ export const CommandPalette = (): JSX.Element => {
label: "Pink", label: "Pink",
icon: ( icon: (
<span <span
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${darkMode ? "bg-[#e454c4]" : "bg-[#dba0c7]"
darkMode ? "bg-[#e454c4]" : "bg-[#dba0c7]"
}`} }`}
/> />
), ),

8
src/components/DeviceSelector.tsx

@ -3,7 +3,6 @@ import { Separator } from "@components/UI/Seperator.tsx";
import { Code } from "@components/UI/Typography/Code.tsx"; import { Code } from "@components/UI/Typography/Code.tsx";
import { useAppStore } from "@core/stores/appStore.ts"; import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts"; import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { import {
HomeIcon, HomeIcon,
LanguagesIcon, LanguagesIcon,
@ -12,6 +11,8 @@ import {
SearchIcon, SearchIcon,
SunIcon, SunIcon,
} from "lucide-react"; } from "lucide-react";
import { Avatar } from "./UI/Avatar";
import type { JSX } from "react";
export const DeviceSelector = (): JSX.Element => { export const DeviceSelector = (): JSX.Element => {
const { getDevices } = useDeviceStore(); const { getDevices } = useDeviceStore();
@ -44,10 +45,7 @@ export const DeviceSelector = (): JSX.Element => {
}} }}
active={selectedDevice === device.id} active={selectedDevice === device.id}
> >
<Hashicon <Avatar text={device.nodes.get(device.hardware.myNodeNum)?.user?.shortName.toString() ?? "UNK"} />
size={24}
value={device.hardware.myNodeNum.toString()}
/>
</DeviceSelectorButton> </DeviceSelectorButton>
))} ))}
<Separator /> <Separator />

6
src/components/DeviceSelectorButton.tsx

@ -1,3 +1,5 @@
import type { JSX } from "react";
export interface DeviceSelectorButtonProps { export interface DeviceSelectorButtonProps {
active: boolean; active: boolean;
onClick: () => void; onClick: () => void;
@ -14,9 +16,9 @@ export const DeviceSelectorButton = ({
onClick={onClick} onClick={onClick}
onKeyDown={onClick} onKeyDown={onClick}
> >
{active && ( {/* {active && (
<div className="absolute -left-2 h-10 w-1.5 rounded-full bg-accent" /> <div className="absolute -left-2 h-10 w-1.5 rounded-full bg-accent" />
)} )} */}
<div className="flex aspect-square cursor-pointer flex-col items-center justify-center"> <div className="flex aspect-square cursor-pointer flex-col items-center justify-center">
{children} {children}
</div> </div>

9
src/components/PageComponents/Map/NodeDetail.tsx

@ -3,7 +3,6 @@ import { H5 } from "@app/components/UI/Typography/H5.tsx";
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx"; import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
import { Separator } from "@app/components/UI/Seperator"; import { Separator } from "@app/components/UI/Seperator";
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx"; import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf } from "@meshtastic/js"; import { Protobuf } from "@meshtastic/js";
import type { Protobuf as ProtobufType } from "@meshtastic/js"; import type { Protobuf as ProtobufType } from "@meshtastic/js";
import { import {
@ -18,6 +17,7 @@ import {
Star, Star,
} from "lucide-react"; } from "lucide-react";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { Avatar } from "@app/components/UI/Avatar";
export interface NodeDetailProps { export interface NodeDetailProps {
node: ProtobufType.Mesh.NodeInfo; node: ProtobufType.Mesh.NodeInfo;
@ -25,6 +25,7 @@ export interface NodeDetailProps {
export const NodeDetail = ({ node }: NodeDetailProps): JSX.Element => { export const NodeDetail = ({ node }: NodeDetailProps): JSX.Element => {
const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`; const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`;
const shortName = node.user?.shortName || `!${numberToHexUnpadded(node.num)}`;
const hardwareType = Protobuf.Mesh.HardwareModel[ const hardwareType = Protobuf.Mesh.HardwareModel[
node.user?.hwModel ?? 0 node.user?.hwModel ?? 0
].replaceAll("_", " "); ].replaceAll("_", " ");
@ -33,8 +34,7 @@ export const NodeDetail = ({ node }: NodeDetailProps): JSX.Element => {
<div className="dark:text-black"> <div className="dark:text-black">
<div className="flex gap-2"> <div className="flex gap-2">
<div className="flex flex-col items-center gap-2 min-w-6 pt-1"> <div className="flex flex-col items-center gap-2 min-w-6 pt-1">
<Hashicon value={node.num.toString()} size={22} /> <Avatar text={shortName.toString()} />
<div> <div>
{node.user?.publicKey && node.user?.publicKey.length > 0 ? ( {node.user?.publicKey && node.user?.publicKey.length > 0 ? (
<LockIcon <LockIcon
@ -68,8 +68,7 @@ export const NodeDetail = ({ node }: NodeDetailProps): JSX.Element => {
{!!node.deviceMetrics?.batteryLevel && ( {!!node.deviceMetrics?.batteryLevel && (
<div <div
className="flex items-center gap-1" className="flex items-center gap-1"
title={`${ title={`${node.deviceMetrics?.voltage?.toPrecision(3) ?? "Unknown"
node.deviceMetrics?.voltage?.toPrecision(3) ?? "Unknown"
} volts`} } volts`}
> >
{node.deviceMetrics?.batteryLevel > 100 ? ( {node.deviceMetrics?.batteryLevel > 100 ? (

11
src/components/PageComponents/Messages/Message.tsx

@ -1,11 +1,12 @@
import { Avatar } from "@app/components/UI/Avatar";
import type { MessageWithState } from "@app/core/stores/deviceStore.ts"; import type { MessageWithState } from "@app/core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import type { Protobuf } from "@meshtastic/js"; import type { Protobuf } from "@meshtastic/js";
import { import {
AlertCircleIcon, AlertCircleIcon,
CheckCircle2Icon, CheckCircle2Icon,
CircleEllipsisIcon, CircleEllipsisIcon,
} from "lucide-react"; } from "lucide-react";
import type { JSX } from "react";
export interface MessageProps { export interface MessageProps {
lastMsgSameUser: boolean; lastMsgSameUser: boolean;
@ -28,8 +29,7 @@ export const Message = ({
<AlertCircleIcon size={16} className="my-auto text-textSecondary" /> <AlertCircleIcon size={16} className="my-auto text-textSecondary" />
)} )}
<span <span
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${ className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
}`} }`}
> >
{message.data} {message.data}
@ -39,7 +39,7 @@ export const Message = ({
<div className="mx-4 mt-2 gap-2"> <div className="mx-4 mt-2 gap-2">
<div className="flex gap-2"> <div className="flex gap-2">
<div className="w-6 cursor-pointer"> <div className="w-6 cursor-pointer">
<Hashicon value={(sender?.num ?? 0).toString()} size={32} /> <Avatar text={(sender?.user?.shortName ?? 0).toString()} />
</div> </div>
<span className="cursor-pointer font-medium text-textPrimary"> <span className="cursor-pointer font-medium text-textPrimary">
{sender?.user?.longName ?? "UNK"} {sender?.user?.longName ?? "UNK"}
@ -66,8 +66,7 @@ export const Message = ({
<AlertCircleIcon size={16} className="my-auto text-textSecondary" /> <AlertCircleIcon size={16} className="my-auto text-textSecondary" />
)} )}
<span <span
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${ className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
}`} }`}
> >
{message.data} {message.data}

99
src/components/UI/Avatar.tsx

@ -0,0 +1,99 @@
import { cn } from "@app/core/utils/cn";
import type React from "react";
type RGBColor = {
r: number;
g: number;
b: number;
a: number;
};
interface AvatarProps {
text: string;
size?: "sm" | "md" | "lg" | "xl";
className?: string;
}
// biome-ignore lint/complexity/noStaticOnlyClass: stop being annoying Biome
class ColorUtils {
static hexToRgb(hex: number): RGBColor {
return {
r: (hex & 0xff0000) >> 16,
g: (hex & 0x00ff00) >> 8,
b: hex & 0x0000ff,
a: 255,
};
}
static rgbToHex(color: RGBColor): number {
return (
(Math.round(color.a) << 24) |
(Math.round(color.r) << 16) |
(Math.round(color.g) << 8) |
Math.round(color.b)
);
}
static isLight(color: RGBColor): boolean {
const brightness = (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
return brightness > 127.5;
}
}
export const Avatar: React.FC<AvatarProps> = ({
text,
size = "md",
className,
}) => {
console.log(text);
const sizes = {
sm: "size-12 text-sm",
md: "size-12 text-sm",
lg: "size-16 text-lg",
xl: "size-29 text-xl",
};
// Generate color from text
const getColorFromText = (): RGBColor => {
let hash = 0;
for (let i = 0; i < text.length; i++) {
hash = text.charCodeAt(i) + ((hash << 5) - hash);
}
return {
r: (hash & 0xff0000) >> 16,
g: (hash & 0x00ff00) >> 8,
b: hash & 0x0000ff,
a: 255,
};
};
const bgColor = getColorFromText();
const isLight = ColorUtils.isLight(bgColor);
const textColor = isLight ? "#000000" : "#FFFFFF";
const initials = text
.toUpperCase()
.slice(0, 4);
return (
<div
className={cn(`
rounded-full
flex
items-center
justify-center
font-semibold`,
sizes[size],
className)
}
style={{
backgroundColor: `rgb(${bgColor.r}, ${bgColor.g}, ${bgColor.b})`,
color: textColor,
}}
>
<p className="p-1">{initials}</p>
</div>
);
};

8
src/components/UI/Sidebar/SidebarSection.tsx

@ -1,17 +1,21 @@
import { cn } from "@app/core/utils/cn";
import { H4 } from "@components/UI/Typography/H4.tsx"; import { H4 } from "@components/UI/Typography/H4.tsx";
import type { JSX } from "react";
export interface SidebarSectionProps { export interface SidebarSectionProps {
label: string; label: string;
subheader?: string; subheader?: string;
children: React.ReactNode; children: React.ReactNode;
className?: string;
} }
export const SidebarSection = ({ export const SidebarSection = ({
label: title, label: title,
children, children,
className,
}: SidebarSectionProps): JSX.Element => ( }: SidebarSectionProps): JSX.Element => (
<div className="px-4 py-2"> <div className="px-4 py-2">
<H4 className="mb-2 ml-2">{title}</H4> <H4 className="mb-3 ml-2">{title}</H4>
<div className="space-y-1">{children}</div> <div className={cn("space-y-1", className)}>{children}</div>
</div> </div>
); );

6
src/components/UI/Sidebar/sidebarButton.tsx

@ -1,5 +1,7 @@
import { cn } from "@app/core/utils/cn";
import { Button } from "@components/UI/Button.tsx"; import { Button } from "@components/UI/Button.tsx";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import type { JSX } from "react";
export interface SidebarButtonProps { export interface SidebarButtonProps {
label: string; label: string;
@ -7,6 +9,7 @@ export interface SidebarButtonProps {
Icon?: LucideIcon; Icon?: LucideIcon;
element?: JSX.Element; element?: JSX.Element;
onClick?: () => void; onClick?: () => void;
className?: string;
} }
export const SidebarButton = ({ export const SidebarButton = ({
@ -15,12 +18,13 @@ export const SidebarButton = ({
Icon, Icon,
element, element,
onClick, onClick,
className
}: SidebarButtonProps): JSX.Element => ( }: SidebarButtonProps): JSX.Element => (
<Button <Button
onClick={onClick} onClick={onClick}
variant={active ? "subtle" : "ghost"} variant={active ? "subtle" : "ghost"}
size="sm" size="sm"
className="w-full justify-start gap-2" className={cn("w-full justify-start gap-2", className)}
> >
{Icon && <Icon size={16} />} {Icon && <Icon size={16} />}
{element && element} {element && element}

19
src/pages/Map.tsx

@ -7,7 +7,6 @@ import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx"; import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
import { useAppStore } from "@core/stores/appStore.ts"; import { useAppStore } from "@core/stores/appStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { bbox, lineString } from "@turf/turf"; import { bbox, lineString } from "@turf/turf";
import { import {
@ -16,10 +15,11 @@ import {
ZoomInIcon, ZoomInIcon,
ZoomOutIcon, ZoomOutIcon,
} from "lucide-react"; } from "lucide-react";
import { useCallback, useEffect, useState } from "react"; import { type JSX, useCallback, useEffect, useState } from "react";
import { AttributionControl, Marker, Popup, useMap } from "react-map-gl"; import { AttributionControl, Marker, Popup, useMap } from "react-map-gl";
import MapGl from "react-map-gl/maplibre"; import MapGl from "react-map-gl/maplibre";
import { Protobuf } from "@meshtastic/js"; import type { Protobuf } from "@meshtastic/js";
import { Avatar } from "@app/components/UI/Avatar";
export const MapPage = (): JSX.Element => { export const MapPage = (): JSX.Element => {
const { nodes, waypoints } = useDevice(); const { nodes, waypoints } = useDevice();
@ -136,7 +136,7 @@ export const MapPage = (): JSX.Element => {
maxPitch={0} maxPitch={0}
style={{ style={{
filter: darkMode filter: darkMode
? "brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)" ? "brightness(0.8)"
: "", : "",
}} }}
dragRotate={false} dragRotate={false}
@ -148,7 +148,10 @@ export const MapPage = (): JSX.Element => {
}} }}
> >
<AttributionControl <AttributionControl
style={{ background: darkMode ? "#ffffff" : "", color: darkMode ? "black" : "" }} style={{
background: darkMode ? "#ffffff" : "",
color: darkMode ? "black" : "",
}}
/> />
{waypoints.map((wp) => ( {waypoints.map((wp) => (
<Marker <Marker
@ -174,7 +177,7 @@ export const MapPage = (): JSX.Element => {
key={node.num} key={node.num}
longitude={(node.position.longitudeI ?? 0) / 1e7} longitude={(node.position.longitudeI ?? 0) / 1e7}
latitude={(node.position.latitudeI ?? 0) / 1e7} latitude={(node.position.latitudeI ?? 0) / 1e7}
style={{ filter: darkMode ? "invert(1)" : "" }} // style={{ filter: darkMode ? "invert(1)" : "" }}
anchor="bottom" anchor="bottom"
onClick={() => { onClick={() => {
setSelectedNode(node); setSelectedNode(node);
@ -187,8 +190,8 @@ export const MapPage = (): JSX.Element => {
}); });
}} }}
> >
<div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5"> <div className="flex cursor-pointer gap-2 rounded-md bg-transparent p-1.5">
<Hashicon value={node.num.toString()} size={22} /> <Avatar text={node.user?.shortName.toString() ?? node.num.toString()} size="sm" />
<Subtle className={cn(zoom < 12 && "hidden")}> <Subtle className={cn(zoom < 12 && "hidden")}>
{node.user?.longName || {node.user?.longName ||
`!${numberToHexUnpadded(node.num)}`} `!${numberToHexUnpadded(node.num)}`}

13
src/pages/Messages.tsx

@ -1,3 +1,4 @@
import { Avatar } from "@app/components/UI/Avatar";
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx"; import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx";
import { PageLayout } from "@components/PageLayout.tsx"; import { PageLayout } from "@components/PageLayout.tsx";
import { Sidebar } from "@components/Sidebar.tsx"; import { Sidebar } from "@components/Sidebar.tsx";
@ -5,12 +6,11 @@ import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx"; import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
import { useToast } from "@core/hooks/useToast.ts"; import { useToast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf, Types } from "@meshtastic/js"; import { Protobuf, Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { getChannelName } from "@pages/Channels.tsx"; import { getChannelName } from "@pages/Channels.tsx";
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react"; import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
import { useState } from "react"; import { type JSX, useState } from "react";
export const MessagesPage = (): JSX.Element => { export const MessagesPage = (): JSX.Element => {
const { channels, nodes, hardware, messages, traceroutes, connection } = const { channels, nodes, hardware, messages, traceroutes, connection } =
@ -55,7 +55,7 @@ export const MessagesPage = (): JSX.Element => {
/> />
))} ))}
</SidebarSection> </SidebarSection>
<SidebarSection label="Nodes"> <SidebarSection label="Nodes" className="flex flex-col gap-4">
{filteredNodes.map((node) => ( {filteredNodes.map((node) => (
<SidebarButton <SidebarButton
key={node.num} key={node.num}
@ -65,18 +65,17 @@ export const MessagesPage = (): JSX.Element => {
setChatType("direct"); setChatType("direct");
setActiveChat(node.num); setActiveChat(node.num);
}} }}
element={<Hashicon size={20} value={node.num.toString()} />} element={<Avatar text={node.user?.shortName.toString() ?? node.num.toString()} />}
/> />
))} ))}
</SidebarSection> </SidebarSection>
</Sidebar> </Sidebar>
<div className="flex flex-col flex-grow"> <div className="flex flex-col flex-grow">
<PageLayout <PageLayout
label={`Messages: ${ label={`Messages: ${chatType === "broadcast" && currentChannel
chatType === "broadcast" && currentChannel
? getChannelName(currentChannel) ? getChannelName(currentChannel)
: chatType === "direct" && nodes.get(activeChat) : chatType === "direct" && nodes.get(activeChat)
? nodes.get(activeChat)?.user?.longName ?? nodeHex ? (nodes.get(activeChat)?.user?.longName ?? nodeHex)
: "Loading..." : "Loading..."
}`} }`}
actions={ actions={

12
src/pages/Nodes.tsx

@ -6,11 +6,10 @@ import { Mono } from "@components/generic/Mono.tsx";
import { Table } from "@components/generic/Table/index.tsx"; import { Table } from "@components/generic/Table/index.tsx";
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx"; import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf } from "@meshtastic/js"; import { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react"; import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
import { Fragment } from "react"; import { Fragment, type JSX } from "react";
import { base16 } from "rfc4648"; import { base16 } from "rfc4648";
export interface DeleteNoteDialogProps { export interface DeleteNoteDialogProps {
@ -44,7 +43,11 @@ export const NodesPage = (): JSX.Element => {
{ title: "Remove", type: "normal", sortable: false }, { title: "Remove", type: "normal", sortable: false },
]} ]}
rows={filteredNodes.map((node) => [ rows={filteredNodes.map((node) => [
<Hashicon key="icon" size={24} value={node.num.toString()} />, <span
key={node.num}
className="h-3 w-3 rounded-full bg-accent"
/>,
<h1 key="header"> <h1 key="header">
{node.user?.longName ?? {node.user?.longName ??
(node.user?.macaddr (node.user?.macaddr
@ -86,8 +89,7 @@ export const NodesPage = (): JSX.Element => {
{node.lastHeard !== 0 {node.lastHeard !== 0
? node.viaMqtt === false && node.hopsAway === 0 ? node.viaMqtt === false && node.hopsAway === 0
? "Direct" ? "Direct"
: `${node.hopsAway.toString()} ${ : `${node.hopsAway.toString()} ${node.hopsAway > 1 ? "hops" : "hop"
node.hopsAway > 1 ? "hops" : "hop"
} away` } away`
: "-"} : "-"}
{node.viaMqtt === true ? ", via MQTT" : ""} {node.viaMqtt === true ? ", via MQTT" : ""}

Loading…
Cancel
Save