Browse Source

chore: update packages to latest version. feat: replace hashicon lib with vanilla component

pull/385/head
Dan Ditomaso 1 year ago
parent
commit
9b843f6483
  1. 87
      package.json
  2. 4987
      pnpm-lock.yaml
  3. 5
      src/DeviceWrapper.tsx
  4. 12
      src/PageRouter.tsx
  5. 14
      src/components/CommandPalette.tsx
  6. 12
      src/components/DeviceSelector.tsx
  7. 6
      src/components/DeviceSelectorButton.tsx
  8. 2
      src/components/Form/FormSelect.tsx
  9. 6
      src/components/Form/FormWrapper.tsx
  10. 6
      src/components/PageComponents/Map/NodeDetail.tsx
  11. 10
      src/components/PageComponents/Messages/Message.tsx
  12. 94
      src/components/UI/Avatar.tsx
  13. 4
      src/components/UI/Sidebar/SidebarSection.tsx
  14. 15
      src/components/generic/Table/index.tsx
  15. 1
      src/index.css
  16. 3
      src/pages/Channels.tsx
  17. 4
      src/pages/Config/index.tsx
  18. 25
      src/pages/Map.tsx
  19. 43
      src/pages/Messages.tsx
  20. 13
      src/pages/Nodes.tsx

87
package.json

@ -27,65 +27,64 @@
"homepage": "https://meshtastic.org", "homepage": "https://meshtastic.org",
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^1.10.0", "@bufbuild/protobuf": "^1.10.0",
"@emeraldpay/hashicon-react": "^0.5.2",
"@meshtastic/js": "2.3.7-5", "@meshtastic/js": "2.3.7-5",
"@noble/curves": "^1.5.0", "@noble/curves": "^1.8.1",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-checkbox": "^1.1.0", "@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-menubar": "^1.1.1", "@radix-ui/react-menubar": "^1.1.5",
"@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-tooltip": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.7",
"@turf/turf": "^6.5.0", "@turf/turf": "^7.2.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.4",
"crypto-random-string": "^5.0.0", "crypto-random-string": "^5.0.0",
"immer": "^10.1.1", "immer": "^10.1.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.363.0", "lucide-react": "^0.474.0",
"mapbox-gl": "^3.6.0", "mapbox-gl": "^3.9.4",
"maplibre-gl": "4.1.2", "maplibre-gl": "4.1.2",
"react": "^18.3.1", "react": "^19.0.0",
"react-dom": "^18.3.1", "react-dom": "^19.0.0",
"react-hook-form": "^7.52.0", "react-hook-form": "^7.54.2",
"react-map-gl": "7.1.7", "react-map-gl": "7.1.9",
"react-qrcode-logo": "^2.10.0", "react-qrcode-logo": "^3.0.0",
"rfc4648": "^1.5.3", "rfc4648": "^1.5.4",
"timeago-react": "^3.0.6", "timeago-react": "^3.0.6",
"vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-node-polyfills": "^0.23.0",
"zustand": "4.5.2" "zustand": "5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.8.2", "@biomejs/biome": "^1.9.4",
"@rsbuild/core": "^1.0.10", "@rsbuild/core": "^1.2.3",
"@rsbuild/plugin-react": "^1.0.3", "@rsbuild/plugin-react": "^1.1.0",
"@types/chrome": "^0.0.263", "@types/chrome": "^0.0.299",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^20.14.9", "@types/node": "^22.12.0",
"@types/react": "^18.3.3", "@types/react": "^19.0.8",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^19.0.3",
"@types/w3c-web-serial": "^1.0.6", "@types/w3c-web-serial": "^1.0.7",
"@types/web-bluetooth": "^0.0.20", "@types/web-bluetooth": "^0.0.20",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.20",
"gzipper": "^7.2.0", "gzipper": "^8.2.0",
"postcss": "^8.4.38", "postcss": "^8.5.1",
"simple-git-hooks": "^2.11.1", "simple-git-hooks": "^2.11.1",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.6.0",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tar": "^6.2.1", "tar": "^7.4.3",
"typescript": "^5.5.2" "typescript": "^5.7.3"
}, },
"packageManager": "[email protected]" "packageManager": "[email protected]"
} }

4987
pnpm-lock.yaml

File diff suppressed because it is too large

5
src/DeviceWrapper.tsx

@ -7,10 +7,7 @@ export interface DeviceWrapperProps {
device?: Device; device?: Device;
} }
export const DeviceWrapper = ({ export const DeviceWrapper = ({ children, device }: DeviceWrapperProps) => {
children,
device,
}: DeviceWrapperProps): JSX.Element => {
return ( return (
<DeviceContext.Provider value={device}>{children}</DeviceContext.Provider> <DeviceContext.Provider value={device}>{children}</DeviceContext.Provider>
); );

12
src/PageRouter.tsx

@ -1,11 +1,11 @@
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
import { ChannelsPage } from "@pages/Channels.tsx"; import ChannelsPage from "@pages/Channels.tsx";
import { ConfigPage } from "@pages/Config/index.tsx"; import ConfigPage from "@pages/Config/index.tsx";
import { MapPage } from "@pages/Map.tsx"; import MapPage from "@pages/Map.tsx";
import { MessagesPage } from "@pages/Messages.tsx"; import MessagesPage from "@pages/Messages.tsx";
import { NodesPage } from "@pages/Nodes.tsx"; import NodesPage from "@pages/Nodes.tsx";
export const PageRouter = (): JSX.Element => { export const PageRouter = () => {
const { activePage } = useDevice(); const { activePage } = useDevice();
return ( return (
<> <>

14
src/components/CommandPalette.tsx

@ -1,3 +1,4 @@
import { Avatar } from "@components/UI/Avatar";
import { import {
CommandDialog, CommandDialog,
CommandEmpty, CommandEmpty,
@ -8,7 +9,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,
@ -51,11 +51,11 @@ export interface Command {
export interface SubItem { export interface SubItem {
label: string; label: string;
icon: JSX.Element; icon: React.ReactNode;
action: () => void; action: () => void;
} }
export const CommandPalette = (): JSX.Element => { export const CommandPalette = () => {
const { const {
commandPaletteOpen, commandPaletteOpen,
setCommandPaletteOpen, setCommandPaletteOpen,
@ -125,9 +125,11 @@ export const CommandPalette = (): JSX.Element => {
device.nodes.get(device.hardware.myNodeNum)?.user?.longName ?? device.nodes.get(device.hardware.myNodeNum)?.user?.longName ??
device.hardware.myNodeNum.toString(), device.hardware.myNodeNum.toString(),
icon: ( icon: (
<Hashicon <Avatar
size={16} text={
value={device.hardware.myNodeNum.toString()} device.nodes.get(device.hardware.myNodeNum)?.user
?.shortName ?? device.hardware.myNodeNum.toString()
}
/> />
), ),
action() { action() {

12
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 type { JSX } from "react";
import { Avatar } from "./UI/Avatar";
export const DeviceSelector = (): JSX.Element => { export const DeviceSelector = (): JSX.Element => {
const { getDevices } = useDeviceStore(); const { getDevices } = useDeviceStore();
@ -44,9 +45,12 @@ export const DeviceSelector = (): JSX.Element => {
}} }}
active={selectedDevice === device.id} active={selectedDevice === device.id}
> >
<Hashicon <Avatar
size={24} text={
value={device.hardware.myNodeNum.toString()} device.nodes
.get(device.hardware.myNodeNum)
?.user?.shortName.toString() ?? "UNK"
}
/> />
</DeviceSelectorButton> </DeviceSelectorButton>
))} ))}

6
src/components/DeviceSelectorButton.tsx

@ -8,15 +8,15 @@ export const DeviceSelectorButton = ({
active, active,
onClick, onClick,
children, children,
}: DeviceSelectorButtonProps): JSX.Element => ( }: DeviceSelectorButtonProps) => (
<li <li
className="aspect-w-1 aspect-h-1 relative w-full" className="aspect-w-1 aspect-h-1 relative w-full"
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>

2
src/components/Form/FormSelect.tsx

@ -51,7 +51,7 @@ export function SelectInput<T extends FieldValues>({
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{optionsEnumValues.map(([name, value]) => ( {optionsEnumValues.map(([name, value]) => (
<SelectItem key={name} value={value.toString()}> <SelectItem key={name + value} value={value.toString()}>
{formatEnumName {formatEnumName
? name ? name
.replace(/_/g, " ") .replace(/_/g, " ")

6
src/components/Form/FormWrapper.tsx

@ -15,9 +15,9 @@ export const FieldWrapper = ({
children, children,
valid, valid,
validationText, validationText,
}: FieldWrapperProps): JSX.Element => ( }: FieldWrapperProps) => (
<div className="pt-6 sm:pt-5"> <div className="pt-6 sm:pt-5">
<div role="group" aria-labelledby="label-notifications"> <fieldset aria-labelledby="label-notifications">
<div className="sm:grid sm:grid-cols-3 sm:items-baseline sm:gap-4"> <div className="sm:grid sm:grid-cols-3 sm:items-baseline sm:gap-4">
<Label>{label}</Label> <Label>{label}</Label>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
@ -32,6 +32,6 @@ export const FieldWrapper = ({
</div> </div>
</div> </div>
</div> </div>
</div> </fieldset>
</div> </div>
); );

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

@ -1,9 +1,9 @@
import { Separator } from "@app/components/UI/Seperator"; import { Separator } from "@app/components/UI/Seperator";
import { H5 } from "@app/components/UI/Typography/H5.tsx"; 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 { Avatar } from "@components/UI/Avatar";
import { Mono } from "@components/generic/Mono.tsx"; import { Mono } from "@components/generic/Mono.tsx";
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 { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
@ -23,7 +23,7 @@ export interface NodeDetailProps {
node: ProtobufType.Mesh.NodeInfo; node: ProtobufType.Mesh.NodeInfo;
} }
export const NodeDetail = ({ node }: NodeDetailProps): JSX.Element => { export const NodeDetail = ({ node }: NodeDetailProps) => {
const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`; const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`;
const hardwareType = Protobuf.Mesh.HardwareModel[ const hardwareType = Protobuf.Mesh.HardwareModel[
node.user?.hwModel ?? 0 node.user?.hwModel ?? 0
@ -33,7 +33,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={node.user?.shortName} />
<div> <div>
{node.user?.publicKey && node.user?.publicKey.length > 0 ? ( {node.user?.publicKey && node.user?.publicKey.length > 0 ? (

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

@ -1,5 +1,5 @@
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 { Avatar } from "@components/UI/Avatar";
import type { Protobuf } from "@meshtastic/js"; import type { Protobuf } from "@meshtastic/js";
import { import {
AlertCircleIcon, AlertCircleIcon,
@ -13,11 +13,7 @@ export interface MessageProps {
sender?: Protobuf.Mesh.NodeInfo; sender?: Protobuf.Mesh.NodeInfo;
} }
export const Message = ({ export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
lastMsgSameUser,
message,
sender,
}: MessageProps): JSX.Element => {
return lastMsgSameUser ? ( return lastMsgSameUser ? (
<div className="ml-5 flex"> <div className="ml-5 flex">
{message.state === "ack" ? ( {message.state === "ack" ? (
@ -39,7 +35,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 ?? "UNK"} />
</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"}

94
src/components/UI/Avatar.tsx

@ -0,0 +1,94 @@
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" | "lg";
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 = "sm",
className,
}) => {
const sizes = {
sm: "size-11 text-xs",
lg: "size-16 text-lg",
};
// Pick a color based on the text provided to function
const getColorFromText = (text: string): 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(text ?? "UNK");
const isLight = ColorUtils.isLight(bgColor);
const textColor = isLight ? "#000000" : "#FFFFFF";
const initials = text?.toUpperCase().slice(0, 4) ?? "UNK";
return (
<div
className={cn(
`
rounded-full
flex
items-center
justify-center
size-11
font-semibold`,
sizes[size],
className,
)}
style={{
backgroundColor: `rgb(${bgColor.r}, ${bgColor.g}, ${bgColor.b})`,
color: textColor,
}}
>
<p className="p-1">{initials}</p>
</div>
);
};

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

@ -9,9 +9,9 @@ export interface SidebarSectionProps {
export const SidebarSection = ({ export const SidebarSection = ({
label: title, label: title,
children, children,
}: SidebarSectionProps): JSX.Element => ( }: SidebarSectionProps) => (
<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="space-y-1">{children}</div>
</div> </div>
); );

15
src/components/generic/Table/index.tsx

@ -74,15 +74,12 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
> >
<div className="flex gap-2"> <div className="flex gap-2">
{heading.title} {heading.title}
{sortColumn === heading.title && ( {sortColumn === heading.title &&
<> (sortOrder === "asc" ? (
{sortOrder === "asc" ? ( <ChevronUpIcon size={16} />
<ChevronUpIcon size={16} /> ) : (
) : ( <ChevronDownIcon size={16} />
<ChevronDownIcon size={16} /> ))}
)}
</>
)}
</div> </div>
</th> </th>
))} ))}

1
src/index.css

@ -97,6 +97,5 @@
} }
img { img {
-drag: none;
-webkit-user-drag: none; -webkit-user-drag: none;
} }

3
src/pages/Channels.tsx

@ -20,7 +20,7 @@ export const getChannelName = (channel: Protobuf.Channel.Channel) =>
? "Primary" ? "Primary"
: `Ch ${channel.index}`; : `Ch ${channel.index}`;
export const ChannelsPage = (): JSX.Element => { const ChannelsPage = () => {
const { channels, setDialogOpen } = useDevice(); const { channels, setDialogOpen } = useDevice();
const [activeChannel, setActiveChannel] = useState<Types.ChannelNumber>( const [activeChannel, setActiveChannel] = useState<Types.ChannelNumber>(
Types.ChannelNumber.Primary, Types.ChannelNumber.Primary,
@ -69,3 +69,4 @@ export const ChannelsPage = (): JSX.Element => {
</> </>
); );
}; };
export default ChannelsPage;

4
src/pages/Config/index.tsx

@ -9,7 +9,7 @@ import { ModuleConfig } from "@pages/Config/ModuleConfig.tsx";
import { BoxesIcon, SaveIcon, SettingsIcon } from "lucide-react"; import { BoxesIcon, SaveIcon, SettingsIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
export const ConfigPage = (): JSX.Element => { const ConfigPage = (): JSX.Element => {
const { workingConfig, workingModuleConfig, connection } = useDevice(); const { workingConfig, workingModuleConfig, connection } = useDevice();
const [activeConfigSection, setActiveConfigSection] = useState< const [activeConfigSection, setActiveConfigSection] = useState<
"device" | "module" "device" | "module"
@ -72,3 +72,5 @@ export const ConfigPage = (): JSX.Element => {
</> </>
); );
}; };
export default ConfigPage;

25
src/pages/Map.tsx

@ -1,4 +1,5 @@
import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail"; import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail";
import { Avatar } from "@app/components/UI/Avatar";
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx"; import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
import { cn } from "@app/core/utils/cn.ts"; import { cn } from "@app/core/utils/cn.ts";
import { PageLayout } from "@components/PageLayout.tsx"; import { PageLayout } from "@components/PageLayout.tsx";
@ -7,7 +8,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 type { Protobuf } from "@meshtastic/js"; import type { Protobuf } from "@meshtastic/js";
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";
@ -17,11 +17,11 @@ import {
ZoomInIcon, ZoomInIcon,
ZoomOutIcon, ZoomOutIcon,
} from "lucide-react"; } from "lucide-react";
import { useCallback, useEffect, useState } from "react"; import { type JSX, useCallback, useEffect, useMemo, 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";
export const MapPage = (): JSX.Element => { const MapPage = (): JSX.Element => {
const { nodes, waypoints } = useDevice(); const { nodes, waypoints } = useDevice();
const { rasterSources, darkMode } = useAppStore(); const { rasterSources, darkMode } = useAppStore();
const { default: map } = useMap(); const { default: map } = useMap();
@ -30,7 +30,7 @@ export const MapPage = (): JSX.Element => {
const [selectedNode, setSelectedNode] = const [selectedNode, setSelectedNode] =
useState<Protobuf.Mesh.NodeInfo | null>(null); useState<Protobuf.Mesh.NodeInfo | null>(null);
const allNodes = Array.from(nodes.values()); const allNodes = useMemo(() => Array.from(nodes.values()), [nodes]);
const getBBox = useCallback(() => { const getBBox = useCallback(() => {
if (!map) { if (!map) {
@ -135,9 +135,7 @@ export const MapPage = (): JSX.Element => {
renderWorldCopies={false} renderWorldCopies={false}
maxPitch={0} maxPitch={0}
style={{ style={{
filter: darkMode filter: darkMode ? "brightness(0.8)" : "",
? "brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)"
: "",
}} }}
dragRotate={false} dragRotate={false}
touchZoomRotate={false} touchZoomRotate={false}
@ -177,7 +175,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);
@ -190,8 +188,13 @@ 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)}`}
@ -217,3 +220,5 @@ export const MapPage = (): JSX.Element => {
</> </>
); );
}; };
export default MapPage;

43
src/pages/Messages.tsx

@ -1,18 +1,18 @@
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";
import { Avatar } from "@components/UI/Avatar.tsx";
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx"; 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 { Device, useDevice, useDeviceStore } 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 { useState } from "react";
export const MessagesPage = (): JSX.Element => { const MessagesPage = () => {
const { channels, nodes, hardware, messages, traceroutes, connection } = const { channels, nodes, hardware, messages, traceroutes, connection } =
useDevice(); useDevice();
const [chatType, setChatType] = const [chatType, setChatType] =
@ -56,18 +56,27 @@ export const MessagesPage = (): JSX.Element => {
))} ))}
</SidebarSection> </SidebarSection>
<SidebarSection label="Nodes"> <SidebarSection label="Nodes">
{filteredNodes.map((node) => ( <div className="flex flex-col gap-4">
<SidebarButton {filteredNodes.map((node) => (
key={node.num} <SidebarButton
label={node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`} key={node.num}
active={activeChat === node.num} label={
onClick={() => { node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`
setChatType("direct"); }
setActiveChat(node.num); active={activeChat === node.num}
}} onClick={() => {
element={<Hashicon size={20} value={node.num.toString()} />} setChatType("direct");
/> setActiveChat(node.num);
))} }}
element={
<Avatar
text={node.user?.shortName ?? node.num.toString()}
size="sm"
/>
}
/>
))}
</div>
</SidebarSection> </SidebarSection>
</Sidebar> </Sidebar>
<div className="flex flex-col flex-grow"> <div className="flex flex-col flex-grow">
@ -76,7 +85,7 @@ export const MessagesPage = (): JSX.Element => {
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={
@ -146,3 +155,5 @@ export const MessagesPage = (): JSX.Element => {
</> </>
); );
}; };
export default MessagesPage;

13
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 {
@ -18,7 +17,7 @@ export interface DeleteNoteDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const NodesPage = (): JSX.Element => { const NodesPage = (): JSX.Element => {
const { nodes, hardware, setDialogOpen } = useDevice(); const { nodes, hardware, setDialogOpen } = useDevice();
const { setNodeNumToBeRemoved } = useAppStore(); const { setNodeNumToBeRemoved } = useAppStore();
@ -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
@ -111,3 +114,5 @@ export const NodesPage = (): JSX.Element => {
</> </>
); );
}; };
export default NodesPage;

Loading…
Cancel
Save