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

6
src/components/DeviceSelectorButton.tsx

@ -1,3 +1,5 @@
import type { JSX } from "react";
export interface DeviceSelectorButtonProps {
active: boolean;
onClick: () => void;
@ -14,9 +16,9 @@ export const DeviceSelectorButton = ({
onClick={onClick}
onKeyDown={onClick}
>
{active && (
{/* {active && (
<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">
{children}
</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 { Separator } from "@app/components/UI/Seperator";
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf } from "@meshtastic/js";
import type { Protobuf as ProtobufType } from "@meshtastic/js";
import {
@ -18,6 +17,7 @@ import {
Star,
} from "lucide-react";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { Avatar } from "@app/components/UI/Avatar";
export interface NodeDetailProps {
node: ProtobufType.Mesh.NodeInfo;
@ -25,6 +25,7 @@ export interface NodeDetailProps {
export const NodeDetail = ({ node }: NodeDetailProps): JSX.Element => {
const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`;
const shortName = node.user?.shortName || `!${numberToHexUnpadded(node.num)}`;
const hardwareType = Protobuf.Mesh.HardwareModel[
node.user?.hwModel ?? 0
].replaceAll("_", " ");
@ -33,8 +34,7 @@ export const NodeDetail = ({ node }: NodeDetailProps): JSX.Element => {
<div className="dark:text-black">
<div className="flex gap-2">
<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>
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
<LockIcon
@ -68,8 +68,7 @@ export const NodeDetail = ({ node }: NodeDetailProps): JSX.Element => {
{!!node.deviceMetrics?.batteryLevel && (
<div
className="flex items-center gap-1"
title={`${
node.deviceMetrics?.voltage?.toPrecision(3) ?? "Unknown"
title={`${node.deviceMetrics?.voltage?.toPrecision(3) ?? "Unknown"
} volts`}
>
{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 { Hashicon } from "@emeraldpay/hashicon-react";
import type { Protobuf } from "@meshtastic/js";
import {
AlertCircleIcon,
CheckCircle2Icon,
CircleEllipsisIcon,
} from "lucide-react";
import type { JSX } from "react";
export interface MessageProps {
lastMsgSameUser: boolean;
@ -28,8 +29,7 @@ export const Message = ({
<AlertCircleIcon size={16} className="my-auto text-textSecondary" />
)}
<span
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${
message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
}`}
>
{message.data}
@ -39,7 +39,7 @@ export const Message = ({
<div className="mx-4 mt-2 gap-2">
<div className="flex gap-2">
<div className="w-6 cursor-pointer">
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
<Avatar text={(sender?.user?.shortName ?? 0).toString()} />
</div>
<span className="cursor-pointer font-medium text-textPrimary">
{sender?.user?.longName ?? "UNK"}
@ -66,8 +66,7 @@ export const Message = ({
<AlertCircleIcon size={16} className="my-auto text-textSecondary" />
)}
<span
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${
message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
}`}
>
{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 type { JSX } from "react";
export interface SidebarSectionProps {
label: string;
subheader?: string;
children: React.ReactNode;
className?: string;
}
export const SidebarSection = ({
label: title,
children,
className,
}: SidebarSectionProps): JSX.Element => (
<div className="px-4 py-2">
<H4 className="mb-2 ml-2">{title}</H4>
<div className="space-y-1">{children}</div>
<H4 className="mb-3 ml-2">{title}</H4>
<div className={cn("space-y-1", className)}>{children}</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 type { LucideIcon } from "lucide-react";
import type { JSX } from "react";
export interface SidebarButtonProps {
label: string;
@ -7,6 +9,7 @@ export interface SidebarButtonProps {
Icon?: LucideIcon;
element?: JSX.Element;
onClick?: () => void;
className?: string;
}
export const SidebarButton = ({
@ -15,12 +18,13 @@ export const SidebarButton = ({
Icon,
element,
onClick,
className
}: SidebarButtonProps): JSX.Element => (
<Button
onClick={onClick}
variant={active ? "subtle" : "ghost"}
size="sm"
className="w-full justify-start gap-2"
className={cn("w-full justify-start gap-2", className)}
>
{Icon && <Icon size={16} />}
{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 { useAppStore } from "@core/stores/appStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { bbox, lineString } from "@turf/turf";
import {
@ -16,10 +15,11 @@ import {
ZoomInIcon,
ZoomOutIcon,
} 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 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 => {
const { nodes, waypoints } = useDevice();
@ -136,7 +136,7 @@ export const MapPage = (): JSX.Element => {
maxPitch={0}
style={{
filter: darkMode
? "brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)"
? "brightness(0.8)"
: "",
}}
dragRotate={false}
@ -148,7 +148,10 @@ export const MapPage = (): JSX.Element => {
}}
>
<AttributionControl
style={{ background: darkMode ? "#ffffff" : "", color: darkMode ? "black" : "" }}
style={{
background: darkMode ? "#ffffff" : "",
color: darkMode ? "black" : "",
}}
/>
{waypoints.map((wp) => (
<Marker
@ -174,7 +177,7 @@ export const MapPage = (): JSX.Element => {
key={node.num}
longitude={(node.position.longitudeI ?? 0) / 1e7}
latitude={(node.position.latitudeI ?? 0) / 1e7}
style={{ filter: darkMode ? "invert(1)" : "" }}
// style={{ filter: darkMode ? "invert(1)" : "" }}
anchor="bottom"
onClick={() => {
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">
<Hashicon value={node.num.toString()} size={22} />
<div className="flex cursor-pointer gap-2 rounded-md bg-transparent p-1.5">
<Avatar text={node.user?.shortName.toString() ?? node.num.toString()} size="sm" />
<Subtle className={cn(zoom < 12 && "hidden")}>
{node.user?.longName ||
`!${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 { PageLayout } from "@components/PageLayout.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 { useToast } from "@core/hooks/useToast.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf, Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { getChannelName } from "@pages/Channels.tsx";
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
import { useState } from "react";
import { type JSX, useState } from "react";
export const MessagesPage = (): JSX.Element => {
const { channels, nodes, hardware, messages, traceroutes, connection } =
@ -55,7 +55,7 @@ export const MessagesPage = (): JSX.Element => {
/>
))}
</SidebarSection>
<SidebarSection label="Nodes">
<SidebarSection label="Nodes" className="flex flex-col gap-4">
{filteredNodes.map((node) => (
<SidebarButton
key={node.num}
@ -65,18 +65,17 @@ export const MessagesPage = (): JSX.Element => {
setChatType("direct");
setActiveChat(node.num);
}}
element={<Hashicon size={20} value={node.num.toString()} />}
element={<Avatar text={node.user?.shortName.toString() ?? node.num.toString()} />}
/>
))}
</SidebarSection>
</Sidebar>
<div className="flex flex-col flex-grow">
<PageLayout
label={`Messages: ${
chatType === "broadcast" && currentChannel
label={`Messages: ${chatType === "broadcast" && currentChannel
? getChannelName(currentChannel)
: chatType === "direct" && nodes.get(activeChat)
? nodes.get(activeChat)?.user?.longName ?? nodeHex
? (nodes.get(activeChat)?.user?.longName ?? nodeHex)
: "Loading..."
}`}
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 { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
import { Fragment } from "react";
import { Fragment, type JSX } from "react";
import { base16 } from "rfc4648";
export interface DeleteNoteDialogProps {
@ -44,7 +43,11 @@ export const NodesPage = (): JSX.Element => {
{ title: "Remove", type: "normal", sortable: false },
]}
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">
{node.user?.longName ??
(node.user?.macaddr
@ -86,8 +89,7 @@ export const NodesPage = (): JSX.Element => {
{node.lastHeard !== 0
? node.viaMqtt === false && node.hopsAway === 0
? "Direct"
: `${node.hopsAway.toString()} ${
node.hopsAway > 1 ? "hops" : "hop"
: `${node.hopsAway.toString()} ${node.hopsAway > 1 ? "hops" : "hop"
} away`
: "-"}
{node.viaMqtt === true ? ", via MQTT" : ""}

Loading…
Cancel
Save