Merge branch 'master' into fix/improve-styling-of-chat
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm check && rsbuild build",
|
"build": "pnpm check && rsbuild build",
|
||||||
|
"build:analyze": "BUNDLE_ANALYZE=true rsbuild build",
|
||||||
"check": "biome check src/",
|
"check": "biome check src/",
|
||||||
"check:fix": "pnpm check --write src/",
|
"check:fix": "pnpm check --write src/",
|
||||||
"format": "biome format --write src/",
|
"format": "biome format --write src/",
|
||||||
@@ -17,6 +18,9 @@
|
|||||||
"simple-git-hooks": {
|
"simple-git-hooks": {
|
||||||
"pre-commit": "npm run check:fix && npm run format"
|
"pre-commit": "npm run check:fix && npm run format"
|
||||||
},
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{ts,tsx}": ["npm run check:fix", "npm run format"]
|
||||||
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/meshtastic/web.git"
|
"url": "git+https://github.com/meshtastic/web.git"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const Device = (): JSX.Element => {
|
|||||||
"Lost and Found":
|
"Lost and Found":
|
||||||
Protobuf.Config.Config_DeviceConfig_Role.LOST_AND_FOUND,
|
Protobuf.Config.Config_DeviceConfig_Role.LOST_AND_FOUND,
|
||||||
"TAK Tracker":
|
"TAK Tracker":
|
||||||
Protobuf.Config.Config_DeviceConfig_Role.SENSOR,
|
Protobuf.Config.Config_DeviceConfig_Role.TAK_TRACKER,
|
||||||
},
|
},
|
||||||
formatEnumName: true,
|
formatEnumName: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const ChannelChat = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 mt-2 p-4 w-full bg-gray-900">
|
<div className="flex-shrink-0 mt-2 p-4 w-full bg-gray-900">
|
||||||
<MessageInput to={to} channel={channel} />
|
<MessageInput to={to} channel={channel} maxBytes={200} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import { type JSX, useCallback, useMemo, useState } from "react";
|
|||||||
export interface MessageInputProps {
|
export interface MessageInputProps {
|
||||||
to: Types.Destination;
|
to: Types.Destination;
|
||||||
channel: Types.ChannelNumber;
|
channel: Types.ChannelNumber;
|
||||||
|
maxBytes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageInput = ({
|
export const MessageInput = ({
|
||||||
to,
|
to,
|
||||||
channel,
|
channel,
|
||||||
|
maxBytes,
|
||||||
}: MessageInputProps): JSX.Element => {
|
}: MessageInputProps): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
connection,
|
connection,
|
||||||
@@ -24,6 +26,7 @@ export const MessageInput = ({
|
|||||||
} = useDevice();
|
} = useDevice();
|
||||||
const myNodeNum = hardware.myNodeNum;
|
const myNodeNum = hardware.myNodeNum;
|
||||||
const [localDraft, setLocalDraft] = useState(messageDraft);
|
const [localDraft, setLocalDraft] = useState(messageDraft);
|
||||||
|
const [messageBytes, setMessageBytes] = useState(maxBytes);
|
||||||
|
|
||||||
const debouncedSetMessageDraft = useMemo(
|
const debouncedSetMessageDraft = useMemo(
|
||||||
() => debounce(setMessageDraft, 300),
|
() => debounce(setMessageDraft, 300),
|
||||||
@@ -60,8 +63,12 @@ export const MessageInput = ({
|
|||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setLocalDraft(newValue);
|
const byteLength = new Blob([newValue]).size;
|
||||||
debouncedSetMessageDraft(newValue);
|
if (byteLength <= maxBytes) {
|
||||||
|
setLocalDraft(newValue);
|
||||||
|
debouncedSetMessageDraft(newValue);
|
||||||
|
setMessageBytes(maxBytes - byteLength);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -85,6 +92,9 @@ export const MessageInput = ({
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{messageBytes}/{maxBytes}
|
||||||
|
</div>
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
<SendIcon size={16} />
|
<SendIcon size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const PageLayout = ({
|
|||||||
<div className="flex justify-end space-x-4">
|
<div className="flex justify-end space-x-4">
|
||||||
{actions?.map((action, index) => (
|
{actions?.map((action, index) => (
|
||||||
<button
|
<button
|
||||||
key={action.icon.name}
|
key={action.icon.displayName}
|
||||||
type="button"
|
type="button"
|
||||||
className="transition-all hover:text-accent"
|
className="transition-all hover:text-accent"
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import {
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export const Dashboard = () => {
|
export const Dashboard = () => {
|
||||||
const { setConnectDialogOpen } = useAppStore();
|
const { setConnectDialogOpen, setSelectedDevice } = useAppStore();
|
||||||
const { getDevices } = useDeviceStore();
|
const { getDevices } = useDeviceStore();
|
||||||
|
const { darkMode } = useAppStore();
|
||||||
|
|
||||||
const devices = useMemo(() => getDevices(), [getDevices]);
|
const devices = useMemo(() => getDevices(), [getDevices]);
|
||||||
|
|
||||||
@@ -38,7 +39,13 @@ export const Dashboard = () => {
|
|||||||
{devices.map((device) => {
|
{devices.map((device) => {
|
||||||
return (
|
return (
|
||||||
<li key={device.id}>
|
<li key={device.id}>
|
||||||
<div className="px-4 py-4 sm:px-6">
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`w-full px-4 py-4 sm:px-6 ${darkMode ? "hover:bg-slate-800 focus:bg-slate-400 active:bg-slate-600" : "hover:bg-slate-50 focus:bg-slate-50 active:bg-slate-100"}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDevice(device.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="truncate text-sm font-medium text-accent">
|
<p className="truncate text-sm font-medium text-accent">
|
||||||
{device.nodes.get(device.hardware.myNodeNum)?.user
|
{device.nodes.get(device.hardware.myNodeNum)?.user
|
||||||
@@ -75,7 +82,7 @@ export const Dashboard = () => {
|
|||||||
{device.nodes.size === 0 ? 0 : device.nodes.size - 1}
|
{device.nodes.size === 0 ? 0 : device.nodes.size - 1}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
+15
-3
@@ -20,9 +20,12 @@ const MessagesPage = () => {
|
|||||||
const [activeChat, setActiveChat] = useState<number>(
|
const [activeChat, setActiveChat] = useState<number>(
|
||||||
Types.ChannelNumber.Primary,
|
Types.ChannelNumber.Primary,
|
||||||
);
|
);
|
||||||
const filteredNodes = Array.from(nodes.values()).filter(
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
(n) => n.num !== hardware.myNodeNum,
|
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
||||||
);
|
if (node.num === hardware.myNodeNum) return false;
|
||||||
|
const nodeName = node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`;
|
||||||
|
return nodeName.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
});
|
||||||
const allChannels = Array.from(channels.values());
|
const allChannels = Array.from(channels.values());
|
||||||
const filteredChannels = allChannels.filter(
|
const filteredChannels = allChannels.filter(
|
||||||
(ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED,
|
(ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED,
|
||||||
@@ -56,6 +59,15 @@ const MessagesPage = () => {
|
|||||||
))}
|
))}
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
<SidebarSection label="Nodes">
|
<SidebarSection label="Nodes">
|
||||||
|
<div className="p-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search nodes..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full p-2 border border-gray-300 rounded bg-white text-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{filteredNodes.map((node) => (
|
{filteredNodes.map((node) => (
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
|
|||||||
+16
-3
@@ -9,6 +9,7 @@ import { useDevice } from "@core/stores/deviceStore.ts";
|
|||||||
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 { useState } from "react";
|
||||||
import { Fragment, type JSX } from "react";
|
import { Fragment, type JSX } from "react";
|
||||||
import { base16 } from "rfc4648";
|
import { base16 } from "rfc4648";
|
||||||
|
|
||||||
@@ -20,15 +21,27 @@ export interface DeleteNoteDialogProps {
|
|||||||
const NodesPage = (): JSX.Element => {
|
const NodesPage = (): JSX.Element => {
|
||||||
const { nodes, hardware, setDialogOpen } = useDevice();
|
const { nodes, hardware, setDialogOpen } = useDevice();
|
||||||
const { setNodeNumToBeRemoved } = useAppStore();
|
const { setNodeNumToBeRemoved } = useAppStore();
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
|
|
||||||
const filteredNodes = Array.from(nodes.values()).filter(
|
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
||||||
(n) => n.num !== hardware.myNodeNum,
|
if (node.num === hardware.myNodeNum) return false;
|
||||||
);
|
const nodeName = node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`;
|
||||||
|
return nodeName.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
|
<div className="p-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search nodes..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full p-2 border border-gray-300 rounded bg-white text-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="overflow-y-auto h-full">
|
<div className="overflow-y-auto h-full">
|
||||||
<Table
|
<Table
|
||||||
headings={[
|
headings={[
|
||||||
|
|||||||
Reference in New Issue
Block a user