Browse Source

DM's and layout fixes

pull/21/head
Sacha Weatherstone 4 years ago
parent
commit
1a4efc17d7
  1. 16
      package.json
  2. 1039
      pnpm-lock.yaml
  3. 6
      src/components/layout/Sidebar/ButtonNav.tsx
  4. 31
      src/components/layout/Sidebar/Settings/Channels.tsx
  5. 20
      src/components/layout/Sidebar/Settings/radio/channels/panels/ChannelsGroup.tsx
  6. 32
      src/components/layout/Sidebar/index.tsx
  7. 2
      src/components/layout/Sidebar/sections/SidebarOverlay.tsx
  8. 20
      src/components/layout/index.tsx
  9. 3
      src/components/menu/BottomNav.tsx
  10. 1
      src/components/modals/VersionInfo.tsx
  11. 8
      src/core/connection.ts
  12. 98
      src/core/slices/meshtasticSlice.ts
  13. 2
      src/pages/Extensions/Info.tsx
  14. 2
      src/pages/Map/MapContainer.tsx
  15. 111
      src/pages/Messages/ChannelChat.tsx
  16. 40
      src/pages/Messages/DmChat.tsx
  17. 28
      src/pages/Messages/Message.tsx
  18. 31
      src/pages/Messages/MessageBar.tsx
  19. 161
      src/pages/Messages/index.tsx
  20. 106
      src/pages/Nodes/NodeCard.tsx

16
package.json

@ -18,14 +18,14 @@
"@meshtastic/meshtasticjs": "^0.6.39", "@meshtastic/meshtasticjs": "^0.6.39",
"@reduxjs/toolkit": "^1.7.2", "@reduxjs/toolkit": "^1.7.2",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"framer-motion": "^6.2.4", "framer-motion": "^6.2.6",
"graphql-request": "^4.0.0", "graphql-request": "^4.0.0",
"mapbox-gl": "^2.7.0", "mapbox-gl": "^2.7.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-flow-renderer": "^10.0.0-next.38", "react-flow-renderer": "^10.0.0-next.39",
"react-hook-form": "^7.26.1", "react-hook-form": "^7.27.0",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-json-pretty": "^2.2.0", "react-json-pretty": "^2.2.0",
"react-multi-select-component": "^4.2.1", "react-multi-select-component": "^4.2.1",
@ -48,10 +48,10 @@
"@typescript-eslint/eslint-plugin": "^5.11.0", "@typescript-eslint/eslint-plugin": "^5.11.0",
"@typescript-eslint/parser": "^5.11.0", "@typescript-eslint/parser": "^5.11.0",
"@verypossible/eslint-config": "^1.6.1", "@verypossible/eslint-config": "^1.6.1",
"@vitejs/plugin-react": "^1.1.4", "@vitejs/plugin-react": "^1.2.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"babel-plugin-module-resolver": "^4.1.0", "babel-plugin-module-resolver": "^4.1.0",
"eslint": "8.8.0", "eslint": "8.9.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-babel-module": "^5.3.1", "eslint-import-resolver-babel-module": "^5.3.1",
@ -62,11 +62,11 @@
"gzipper": "^7.0.0", "gzipper": "^7.0.0",
"postcss": "^8.4.6", "postcss": "^8.4.6",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"prettier-plugin-tailwindcss": "^0.1.5", "prettier-plugin-tailwindcss": "^0.1.7",
"tailwindcss": "^3.0.19", "tailwindcss": "^3.0.22",
"tar": "^6.1.11", "tar": "^6.1.11",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"vite": "^2.7.13", "vite": "^2.8.1",
"vite-plugin-cdn-import": "^0.3.5", "vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-pwa": "^0.11.13", "vite-plugin-pwa": "^0.11.13",
"workbox-window": "^6.4.2" "workbox-window": "^6.4.2"

1039
pnpm-lock.yaml

File diff suppressed because it is too large

6
src/components/layout/Sidebar/ButtonNav.tsx

@ -4,8 +4,8 @@ import { FiMessageCircle, FiSettings } from 'react-icons/fi';
import { RiMindMap, RiRoadMapLine } from 'react-icons/ri'; import { RiMindMap, RiRoadMapLine } from 'react-icons/ri';
import { VscExtensions } from 'react-icons/vsc'; import { VscExtensions } from 'react-icons/vsc';
import { toggleMobileNav } from '@app/core/slices/appSlice.js'; import { toggleMobileNav } from '@app/core/slices/appSlice';
import { useAppDispatch } from '@app/hooks/useAppDispatch.js'; import { useAppDispatch } from '@app/hooks/useAppDispatch';
import { routes, useRoute } from '@core/router'; import { routes, useRoute } from '@core/router';
import { NavLinkButton } from './NavLinkButton'; import { NavLinkButton } from './NavLinkButton';
@ -21,7 +21,7 @@ export const ButtonNav = ({
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
return ( return (
<div className="z-10 flex justify-between border-t border-gray-300 px-6 py-2 dark:border-gray-600 dark:bg-primaryDark"> <div className="z-30 flex justify-between border-t border-gray-300 px-6 py-2 dark:border-gray-600 dark:bg-primaryDark">
<div <div
onClick={(): void => { onClick={(): void => {
dispatch(toggleMobileNav()); dispatch(toggleMobileNav());

31
src/components/layout/Sidebar/Settings/Channels.tsx

@ -9,7 +9,6 @@ import {
} from 'react-icons/ri'; } from 'react-icons/ri';
import { ListItem } from '@app/components/generic/ListItem'; import { ListItem } from '@app/components/generic/ListItem';
import type { ChannelData } from '@app/core/slices/meshtasticSlice';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Input, Select, Tooltip } from '@meshtastic/components'; import { Checkbox, Input, Select, Tooltip } from '@meshtastic/components';
@ -19,19 +18,19 @@ export const Channels = (): JSX.Element => {
const channels = useAppSelector((state) => state.meshtastic.radio.channels); const channels = useAppSelector((state) => state.meshtastic.radio.channels);
const adminChannel = const adminChannel =
channels.find( channels.find(
(channel) => channel.channel.role === Protobuf.Channel_Role.PRIMARY, (channel) => channel.role === Protobuf.Channel_Role.PRIMARY,
) ?? channels[0]; ) ?? channels[0];
const [usePreset, setUsePreset] = React.useState(true); const [usePreset, setUsePreset] = React.useState(true);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [selectedChannel, setSelectedChannel] = React.useState< const [selectedChannel, setSelectedChannel] = React.useState<
ChannelData | undefined Protobuf.Channel | undefined
>(); >();
const { register, handleSubmit, reset, formState } = useForm< const { register, handleSubmit, reset, formState } = useForm<
DeepOmit<Protobuf.Channel, 'psk'> DeepOmit<Protobuf.Channel, 'psk'>
>({ >({
defaultValues: { defaultValues: {
...adminChannel.channel, ...adminChannel,
}, },
}); });
@ -42,7 +41,7 @@ export const Channels = (): JSX.Element => {
...data, ...data,
settings: { settings: {
...data.settings, ...data.settings,
psk: adminChannel.channel.settings?.psk, psk: adminChannel.settings?.psk,
}, },
}); });
@ -111,7 +110,7 @@ export const Channels = (): JSX.Element => {
)} )}
{channels.map((channel) => ( {channels.map((channel) => (
<ListItem <ListItem
key={channel.channel.index} key={channel.index}
onClick={(): void => { onClick={(): void => {
setSelectedChannel(channel); setSelectedChannel(channel);
}} }}
@ -121,23 +120,23 @@ export const Channels = (): JSX.Element => {
[ [
Protobuf.Channel_Role.SECONDARY, Protobuf.Channel_Role.SECONDARY,
Protobuf.Channel_Role.PRIMARY, Protobuf.Channel_Role.PRIMARY,
].find((role) => role === channel.channel.role) ].find((role) => role === channel.role)
? 'bg-green-500' ? 'bg-green-500'
: 'bg-gray-400' : 'bg-gray-400'
}`} }`}
/> />
} }
selected={selectedChannel?.channel.index === channel.channel.index} selected={selectedChannel?.index === channel.index}
selectedIcon={<FiExternalLink />} selectedIcon={<FiExternalLink />}
actions={ actions={
<Tooltip content={`MQTT Status`}> <Tooltip content={`MQTT Status`}>
<div className="rounded-md p-2"> <div className="rounded-md p-2">
{channel.channel.settings?.uplinkEnabled && {channel.settings?.uplinkEnabled &&
channel.channel.settings?.downlinkEnabled ? ( channel.settings?.downlinkEnabled ? (
<RiArrowUpDownLine className="p-0.5 group-active:scale-90" /> <RiArrowUpDownLine className="p-0.5 group-active:scale-90" />
) : channel.channel.settings?.uplinkEnabled ? ( ) : channel.settings?.uplinkEnabled ? (
<RiArrowUpLine className="p-0.5 group-active:scale-90" /> <RiArrowUpLine className="p-0.5 group-active:scale-90" />
) : channel.channel.settings?.downlinkEnabled ? ( ) : channel.settings?.downlinkEnabled ? (
<RiArrowDownLine className="p-0.5 group-active:scale-90" /> <RiArrowDownLine className="p-0.5 group-active:scale-90" />
) : ( ) : (
<FiX className="p-0.5" /> <FiX className="p-0.5" />
@ -147,11 +146,11 @@ export const Channels = (): JSX.Element => {
} }
> >
<div> <div>
{channel.channel.settings?.name.length {channel.settings?.name.length
? channel.channel.settings.name ? channel.settings.name
: channel.channel.role === Protobuf.Channel_Role.PRIMARY : channel.role === Protobuf.Channel_Role.PRIMARY
? 'Primary' ? 'Primary'
: `Channel: ${channel.channel.index}`} : `Channel: ${channel.index}`}
</div> </div>
</ListItem> </ListItem>
))} ))}

20
src/components/layout/Sidebar/Settings/radio/channels/panels/ChannelsGroup.tsx

@ -17,21 +17,21 @@ export const ChannelsGroup = (): JSX.Element => {
<> <>
{channels.map((channel) => { {channels.map((channel) => {
return ( return (
<div key={channel.channel.index}> <div key={channel.index}>
<CollapsibleSection <CollapsibleSection
title={ title={
channel.channel.settings?.name.length channel.settings?.name.length
? channel.channel.settings.name ? channel.settings.name
: channel.channel.role === Protobuf.Channel_Role.PRIMARY : channel.role === Protobuf.Channel_Role.PRIMARY
? 'Primary' ? 'Primary'
: `Channel: ${channel.channel.index}` : `Channel: ${channel.index}`
} }
icon={ icon={
<div <div
className={`h-3 w-3 rounded-full ${ className={`h-3 w-3 rounded-full ${
channel.channel.role === Protobuf.Channel_Role.PRIMARY channel.role === Protobuf.Channel_Role.PRIMARY
? 'bg-orange-500' ? 'bg-orange-500'
: channel.channel.role === Protobuf.Channel_Role.SECONDARY : channel.role === Protobuf.Channel_Role.SECONDARY
? 'bg-green-500' ? 'bg-green-500'
: 'bg-gray-500' : 'bg-gray-500'
}`} }`}
@ -46,9 +46,9 @@ export const ChannelsGroup = (): JSX.Element => {
} }
> >
<> <>
{/* <DebugPanel channel={channel.channel} /> */} {/* <DebugPanel channel={channel} /> */}
{/* <QRCodePanel channel={channel.channel} /> */} {/* <QRCodePanel channel={channel} /> */}
<SettingsPanel channel={channel.channel} /> <SettingsPanel channel={channel} />
</> </>
</CollapsibleSection> </CollapsibleSection>
</div> </div>

32
src/components/layout/Sidebar/index.tsx

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useAppSelector } from '@app/hooks/useAppSelector.js'; import { useAppSelector } from '@app/hooks/useAppSelector';
import { ButtonNav } from './ButtonNav'; import { ButtonNav } from './ButtonNav';
import { Settings } from './Settings/Index'; import { Settings } from './Settings/Index';
@ -15,23 +15,21 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
const appState = useAppSelector((state) => state.app); const appState = useAppSelector((state) => state.app);
return ( return (
<div className="flex flex-grow"> <div
<div className={`absolute z-20 h-full w-full flex-grow flex-col md:relative md:flex md:w-96 ${
className={`absolute h-full w-full flex-grow flex-col pb-6 md:relative md:flex md:w-96 md:pb-0 ${ appState.mobileNavOpen ? 'flex' : 'hidden'
appState.mobileNavOpen ? 'flex' : 'hidden' }`}
}`} >
> <div className="flex h-full w-full flex-col shadow-xl dark:bg-primaryDark">
<div className="flex h-full w-full flex-col shadow-xl dark:bg-primaryDark"> <div className="relative flex-grow gap-1">
<div className="relative flex-grow gap-1"> <div className="absolute h-full w-full">{children}</div>
<div className="absolute h-full w-full">{children}</div> <Settings open={settingsOpen} setOpen={setSettingsOpen} />
<Settings open={settingsOpen} setOpen={setSettingsOpen} />
</div>
<ButtonNav
toggleSettingsOpen={(): void => {
setSettingsOpen(!settingsOpen);
}}
/>
</div> </div>
<ButtonNav
toggleSettingsOpen={(): void => {
setSettingsOpen(!settingsOpen);
}}
/>
</div> </div>
</div> </div>
); );

2
src/components/layout/Sidebar/sections/SidebarOverlay.tsx

@ -24,7 +24,7 @@ export const SidebarOverlay = ({
<AnimatePresence> <AnimatePresence>
{open && ( {open && (
<m.div <m.div
className="absolute z-10 flex h-full w-full flex-col bg-gray-100 dark:bg-primaryDark" className="absolute z-30 flex h-full w-full flex-col bg-gray-100 dark:bg-primaryDark"
animate={direction === 'x' ? { translateX: 0 } : { translateY: 0 }} animate={direction === 'x' ? { translateX: 0 } : { translateY: 0 }}
initial={ initial={
direction === 'x' ? { translateX: '-100%' } : { translateY: '100%' } direction === 'x' ? { translateX: '-100%' } : { translateY: '100%' }

20
src/components/layout/index.tsx

@ -21,16 +21,18 @@ export const Layout = ({
children, children,
}: LayoutProps): JSX.Element => { }: LayoutProps): JSX.Element => {
return ( return (
<div className="flex w-full bg-gray-100 dark:bg-secondaryDark md:overflow-hidden md:shadow-xl"> <div className="relative flex w-full bg-gray-100 dark:bg-secondaryDark md:overflow-hidden md:shadow-xl">
<Sidebar> <div className="flex flex-grow">
<div className="flex gap-2 border-b border-gray-300 p-2 dark:border-gray-600"> <Sidebar>
<IconButton icon={icon} /> <div className="flex gap-2 border-b border-gray-300 p-2 dark:border-gray-600">
<div className="my-auto text-lg font-medium dark:text-white"> <IconButton icon={icon} />
{title} <div className="my-auto text-lg font-medium dark:text-white">
{title}
</div>
</div> </div>
</div> <div className="flex flex-col gap-2">{sidebarContents}</div>
<div className="flex flex-col gap-2">{sidebarContents}</div> </Sidebar>
</Sidebar> </div>
<ErrorBoundary FallbackComponent={ErrorFallback}> <ErrorBoundary FallbackComponent={ErrorFallback}>
{children} {children}
</ErrorBoundary> </ErrorBoundary>

3
src/components/menu/BottomNav.tsx

@ -36,8 +36,7 @@ export const BottomNav = (): JSX.Element => {
const appState = useAppSelector((state) => state.app); const appState = useAppSelector((state) => state.app);
const primaryChannelSettings = useAppSelector( const primaryChannelSettings = useAppSelector(
(state) => state.meshtastic.radio.channels, (state) => state.meshtastic.radio.channels,
).find((channel) => channel.channel.role === Protobuf.Channel_Role.PRIMARY) ).find((channel) => channel.role === Protobuf.Channel_Role.PRIMARY)?.settings;
?.channel.settings;
return ( return (
<div className="z-20 flex justify-between border-t border-gray-300 bg-white dark:border-gray-600 dark:bg-secondaryDark"> <div className="z-20 flex justify-between border-t border-gray-300 bg-white dark:border-gray-600 dark:bg-secondaryDark">

1
src/components/modals/VersionInfo.tsx

@ -40,7 +40,6 @@ export const VersionInfo = ({
// }`, // }`,
// fetcher, // fetcher,
// ); // );
// console.log(data);
return ( return (
<AnimatePresence> <AnimatePresence>

8
src/core/connection.ts

@ -1,6 +1,7 @@
import { connType } from '@core/slices/appSlice'; import { connType } from '@core/slices/appSlice';
import { import {
addChannel, addChannel,
addChat,
addMessage, addMessage,
addNode, addNode,
addPosition, addPosition,
@ -86,7 +87,6 @@ const registerListeners = (): void => {
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE; SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
connection.onMeshPacket.subscribe((packet) => { connection.onMeshPacket.subscribe((packet) => {
console.log(packet);
store.dispatch( store.dispatch(
addRoute({ addRoute({
from: packet.from, from: packet.from,
@ -127,6 +127,7 @@ const registerListeners = (): void => {
connection.onNodeInfoPacket.subscribe( connection.onNodeInfoPacket.subscribe(
(nodeInfoPacket): void | { payload: Protobuf.NodeInfo; type: string } => { (nodeInfoPacket): void | { payload: Protobuf.NodeInfo; type: string } => {
store.dispatch(addNode(nodeInfoPacket.data)); store.dispatch(addNode(nodeInfoPacket.data));
store.dispatch(addChat(nodeInfoPacket.data.num));
}, },
); );
@ -134,6 +135,9 @@ const registerListeners = (): void => {
switch (adminPacket.data.variant.oneofKind) { switch (adminPacket.data.variant.oneofKind) {
case 'getChannelResponse': case 'getChannelResponse':
store.dispatch(addChannel(adminPacket.data.variant.getChannelResponse)); store.dispatch(addChannel(adminPacket.data.variant.getChannelResponse));
store.dispatch(
addChat(adminPacket.data.variant.getChannelResponse.index),
);
break; break;
case 'getRadioResponse': case 'getRadioResponse':
if (adminPacket.data.variant.getRadioResponse.preferences) { if (adminPacket.data.variant.getRadioResponse.preferences) {
@ -153,8 +157,6 @@ const registerListeners = (): void => {
); );
connection.onRoutingPacket.subscribe((routingPacket) => { connection.onRoutingPacket.subscribe((routingPacket) => {
console.log(routingPacket);
store.dispatch( store.dispatch(
updateLastInteraction({ updateLastInteraction({
id: routingPacket.packet.from, id: routingPacket.packet.from,

98
src/core/slices/meshtasticSlice.ts

@ -9,13 +9,7 @@ export interface MessageWithAck {
} }
export interface Chat { export interface Chat {
id: number; //Channel or user id (for dm's) lastInterraction: Date;
messages: MessageWithAck[];
}
export interface ChannelData {
channel: Protobuf.Channel;
lastChatInterraction: Date;
messages: MessageWithAck[]; messages: MessageWithAck[];
} }
@ -27,6 +21,10 @@ interface CurrentPosition {
satsInView: number; satsInView: number;
} }
type ChatEntries = {
[key in number]: Chat;
};
interface Route { interface Route {
from: number; from: number;
to: number; to: number;
@ -46,7 +44,7 @@ export interface Node {
} }
export interface Radio { export interface Radio {
channels: ChannelData[]; channels: Protobuf.Channel[];
preferences: Protobuf.RadioConfig_UserPreferences; preferences: Protobuf.RadioConfig_UserPreferences;
hardware: Protobuf.MyNodeInfo; hardware: Protobuf.MyNodeInfo;
} }
@ -59,7 +57,7 @@ interface MeshtasticState {
radio: Radio; radio: Radio;
hostOverrideEnabled: boolean; hostOverrideEnabled: boolean;
hostOverride: string; hostOverride: string;
chats: Chat[]; chats: ChatEntries;
} }
const initialState: MeshtasticState = { const initialState: MeshtasticState = {
@ -77,7 +75,7 @@ const initialState: MeshtasticState = {
hostOverrideEnabled: hostOverrideEnabled:
localStorage.getItem('hostOverrideEnabled') === 'true' ?? false, localStorage.getItem('hostOverrideEnabled') === 'true' ?? false,
hostOverride: localStorage.getItem('hostOverride') ?? '', hostOverride: localStorage.getItem('hostOverride') ?? '',
chats: [], chats: {},
}; };
export const meshtasticSlice = createSlice({ export const meshtasticSlice = createSlice({
@ -167,24 +165,16 @@ export const meshtasticSlice = createSlice({
addChannel: (state, action: PayloadAction<Protobuf.Channel>) => { addChannel: (state, action: PayloadAction<Protobuf.Channel>) => {
if ( if (
state.radio.channels.findIndex( state.radio.channels.findIndex(
(channel) => channel.channel.index === action.payload.index, (channel) => channel.index === action.payload.index,
) !== -1 ) !== -1
) { ) {
state.radio.channels = state.radio.channels.map((channel) => { state.radio.channels = state.radio.channels.map((channel) => {
return channel.channel.index === action.payload.index return channel.index === action.payload.index
? { ? action.payload
channel: action.payload,
lastChatInterraction: new Date(),
messages: channel.messages,
}
: channel; : channel;
}); });
} else { } else {
state.radio.channels.push({ state.radio.channels.push(action.payload);
channel: action.payload,
lastChatInterraction: new Date(),
messages: [],
});
} }
}, },
addRoute: (state, action: PayloadAction<Route>) => { addRoute: (state, action: PayloadAction<Route>) => {
@ -195,27 +185,10 @@ export const meshtasticSlice = createSlice({
(route) => (route) =>
route.from === action.payload.from && route.to === action.payload.to, route.from === action.payload.from && route.to === action.payload.to,
); );
console.log(exists);
if (exists === -1) { if (exists === -1) {
node?.routes.push(action.payload); node?.routes.push(action.payload);
} }
// node?.routes.map((route) => {
// if (
// ) {
// node?.routes.push(action.payload);
// }
// });
// if (node) {
// node.routes = node.routes.map((route) => {
// return route.from === action.payload.from &&
// route.to === action.payload.to
// ? action.payload
// : route;
// });
// }
}, },
setPreferences: ( setPreferences: (
state, state,
@ -224,21 +197,38 @@ export const meshtasticSlice = createSlice({
state.radio.preferences = action.payload; state.radio.preferences = action.payload;
}, },
addMessage: (state, action: PayloadAction<MessageWithAck>) => { addMessage: (state, action: PayloadAction<MessageWithAck>) => {
const channelIndex = state.radio.channels.findIndex( console.log(action.payload);
(channel) =>
channel.channel.index === action.payload.message.packet.channel, console.log(
`${action.payload.message.packet.from} -> ${action.payload.message.packet.to}`,
); );
state.radio.channels[channelIndex].messages.push(action.payload); state.chats[action.payload.message.packet.channel].lastInterraction =
state.radio.channels[channelIndex].lastChatInterraction = new Date(); new Date();
if (action.payload.message.packet.to === 0xffffffff) {
console.log('boradcast');
state.chats[action.payload.message.packet.channel].messages.push(
action.payload,
);
} else {
console.log('dm');
const dmIndex =
action.payload.message.packet.from === state.radio.hardware.myNodeNum
? action.payload.message.packet.to
: action.payload.message.packet.from;
state.chats[dmIndex].messages.push(action.payload);
}
}, },
ackMessage: ( ackMessage: (
state, state,
action: PayloadAction<{ channel: number; messageId: number }>, action: PayloadAction<{ chatIndex: number; messageId: number }>,
) => { ) => {
const channelIndex = state.radio.channels.findIndex( console.log(action.payload);
(channel) => channel.channel.index === action.payload.channel,
); state.chats[action.payload.chatIndex].messages.map((message) => {
state.radio.channels[channelIndex].messages.map((message) => {
if (message.message.packet.id === action.payload.messageId) { if (message.message.packet.id === action.payload.messageId) {
message.ack = true; message.ack = true;
} }
@ -269,10 +259,11 @@ export const meshtasticSlice = createSlice({
// connection.disconnect(); // connection.disconnect();
} }
}, },
addChat: (state, action: PayloadAction<Chat>) => { addChat: (state, action: PayloadAction<number>) => {
if (state.chats.findIndex((chat) => chat.id === action.payload.id)) { state.chats[action.payload] = {
state.chats.push(action.payload); messages: [],
} lastInterraction: new Date(),
};
}, },
resetState: (state) => { resetState: (state) => {
state.deviceStatus = Types.DeviceStatusEnum.DEVICE_DISCONNECTED; state.deviceStatus = Types.DeviceStatusEnum.DEVICE_DISCONNECTED;
@ -300,6 +291,7 @@ export const {
updateLastInteraction, updateLastInteraction,
setHostOverrideEnabled, setHostOverrideEnabled,
setHostOverride, setHostOverride,
addChat,
resetState, resetState,
} = meshtasticSlice.actions; } = meshtasticSlice.actions;

2
src/pages/Extensions/Info.tsx

@ -3,7 +3,7 @@ import React from 'react';
import { m } from 'framer-motion'; import { m } from 'framer-motion';
import JSONPretty from 'react-json-pretty'; import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector.js'; import { useAppSelector } from '@app/hooks/useAppSelector';
import { Hashicon } from '@emeraldpay/hashicon-react'; import { Hashicon } from '@emeraldpay/hashicon-react';
export const Info = (): JSX.Element => { export const Info = (): JSX.Element => {

2
src/pages/Map/MapContainer.tsx

@ -41,7 +41,7 @@ export const MapContainer = (): JSX.Element => {
return ( return (
<div className="relative flex h-full w-full"> <div className="relative flex h-full w-full">
<div className="absolute right-0 z-20 m-4 space-y-2 rounded-md border border-gray-300 bg-white p-2 shadow-md dark:border-gray-600 dark:bg-primaryDark"> <div className="absolute right-0 z-10 m-4 space-y-2 rounded-md border border-gray-300 bg-white p-2 shadow-md dark:border-gray-600 dark:bg-primaryDark">
<IconButton <IconButton
active={mapState.style === 'Satellite'} active={mapState.style === 'Satellite'}
onClick={(): void => { onClick={(): void => {

111
src/pages/Messages/ChannelChat.tsx

@ -0,0 +1,111 @@
import React from 'react';
import { m } from 'framer-motion';
import { FiSettings } from 'react-icons/fi';
import { MdPublic } from 'react-icons/md';
import TimeAgo from 'timeago-react';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import { Hashicon } from '@emeraldpay/hashicon-react';
import { useAppSelector } from '@hooks/useAppSelector';
import { IconButton, Tooltip } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface ChannelChatProps {
channel: Protobuf.Channel;
selectedIndex: number;
setSelectedIndex: (index: number) => void;
}
export const ChannelChat = ({
channel,
selectedIndex,
setSelectedIndex,
}: ChannelChatProps): JSX.Element => {
const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware,
).myNodeNum;
const nodes = useAppSelector((state) => state.meshtastic.nodes).filter(
(node) => node.number !== myNodeNum,
);
const chats = useAppSelector((state) => state.meshtastic.chats);
const channels = useAppSelector(
(state) => state.meshtastic.radio.channels,
).filter((ch) => ch.role !== Protobuf.Channel_Role.DISABLED);
return (
<SidebarItem
key={channel.index}
selected={channel.index === selectedIndex}
setSelected={(): void => {
setSelectedIndex(channel.index);
}}
actions={<IconButton icon={<FiSettings />} />}
>
<Tooltip
content={
channel.settings?.name.length
? channel.settings.name
: `CH: ${channel.index}`
}
>
<div className="flex h-8 w-8 rounded-full bg-gray-200 dark:bg-primaryDark dark:text-white">
<div className="m-auto">
{channel.role === Protobuf.Channel_Role.PRIMARY ? (
<MdPublic />
) : (
<p>
{channel.settings?.name.length
? channel.settings.name.substring(0, 3).toUpperCase()
: `CH: ${channel.index}`}
</p>
)}
</div>
</div>
</Tooltip>
{chats[channel.index]?.messages.length ? (
<>
<div className="mx-2 flex h-8">
{[
...new Set(
chats[channel.index]?.messages.flatMap(({ message }) => [
message.packet.from,
]),
),
]
.sort()
.map((nodeId) => {
return (
<Tooltip
key={nodeId}
content={
nodes.find((node) => node.number === nodeId)?.user
?.longName ?? 'UNK'
}
>
<div className="flex h-full">
<m.div
whileHover={{ scale: 1.1 }}
className="my-auto -ml-2"
>
<Hashicon value={nodeId.toString()} size={20} />
</m.div>
</div>
</Tooltip>
);
})}
</div>
<div className="my-auto ml-auto text-xs font-semibold dark:text-gray-400">
{chats[channel.index].messages.length ? (
<TimeAgo datetime={chats[channel.index].lastInterraction} />
) : (
<div>No messages</div>
)}
</div>
</>
) : (
<div className="my-auto dark:text-white">No messages</div>
)}
</SidebarItem>
);
};

40
src/pages/Messages/DmChat.tsx

@ -0,0 +1,40 @@
import React from 'react';
import { FiSettings } from 'react-icons/fi';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import type { Node } from '@core/slices/meshtasticSlice';
import { Hashicon } from '@emeraldpay/hashicon-react';
import { IconButton } from '@meshtastic/components';
export interface DmChatProps {
node: Node;
selectedIndex: number;
setSelectedIndex: (index: number) => void;
}
export const DmChat = ({
node,
selectedIndex,
setSelectedIndex,
}: DmChatProps): JSX.Element => {
return (
<SidebarItem
key={node.number}
selected={node.number === selectedIndex}
setSelected={(): void => {
setSelectedIndex(node.number);
}}
actions={<IconButton icon={<FiSettings />} />}
>
<div className="flex dark:text-white">
<div className="m-auto">
<Hashicon value={node.number.toString()} size={32} />
</div>
</div>
<div className="my-auto mr-auto font-semibold dark:text-white">
{node.user?.longName ?? 'Unknown'}
</div>
</SidebarItem>
);
};

28
src/pages/Messages/Message.tsx

@ -1,5 +1,7 @@
import type React from 'react'; import type React from 'react';
import { FiClock } from 'react-icons/fi';
import type { Node } from '@app/core/slices/meshtasticSlice'; import type { Node } from '@app/core/slices/meshtasticSlice';
import { Hashicon } from '@emeraldpay/hashicon-react'; import { Hashicon } from '@emeraldpay/hashicon-react';
import { Tooltip } from '@meshtastic/components'; import { Tooltip } from '@meshtastic/components';
@ -23,9 +25,11 @@ export const Message = ({
<div className="group mb-3 hover:bg-gray-200 dark:hover:bg-primaryDark"> <div className="group mb-3 hover:bg-gray-200 dark:hover:bg-primaryDark">
{lastMsgSameUser ? ( {lastMsgSameUser ? (
<div <div
className={`mx-6 -mt-3 flex gap-2 ${lastMsgSameUser ? '' : 'py-1'}`} className={`mx-6 -mt-3 flex justify-between ${
lastMsgSameUser ? '' : 'py-1'
}`}
> >
<div className="flex"> <div className="flex gap-2">
<Tooltip content={rxTime.toString()}> <Tooltip content={rxTime.toString()}>
<div className="my-auto ml-auto w-8 pt-1 text-xs text-transparent dark:group-hover:text-gray-400"> <div className="my-auto ml-auto w-8 pt-1 text-xs text-transparent dark:group-hover:text-gray-400">
{rxTime {rxTime
@ -37,14 +41,20 @@ export const Message = ({
.replace('PM', '')} .replace('PM', '')}
</div> </div>
</Tooltip> </Tooltip>
<div
className={`my-auto dark:text-gray-300 ${
ack ? '' : 'animate-pulse dark:text-gray-500'
}`}
>
{message}
</div>
</div> </div>
<div <Tooltip content="Response time">
className={`my-auto dark:text-gray-300 ${ <div className="flex gap-1 pt-1 text-xs text-transparent dark:group-hover:text-gray-400">
ack ? '' : 'animate-pulse dark:text-gray-500' <FiClock className="mt-0.5" />
}`} <div>25s</div>
> </div>
{message} </Tooltip>
</div>
</div> </div>
) : ( ) : (
<div className="mx-6 flex gap-2"> <div className="mx-6 flex gap-2">

31
src/pages/Messages/MessageBar.tsx

@ -7,23 +7,38 @@ import { useAppSelector } from '@hooks/useAppSelector';
import { Input } from '@meshtastic/components'; import { Input } from '@meshtastic/components';
export interface MessageBarProps { export interface MessageBarProps {
channelIndex: number; chatIndex: number;
} }
export const MessageBar = ({ channelIndex }: MessageBarProps): JSX.Element => { export const MessageBar = ({ chatIndex }: MessageBarProps): JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const ready = useAppSelector((state) => state.meshtastic.ready); const meshtasticState = useAppSelector((state) => state.meshtastic);
const [isChannel, setIsChannel] = React.useState(false);
React.useState(() => {
setIsChannel(
meshtasticState.radio.channels.findIndex(
(channel) => channel.index === chatIndex,
) !== -1,
);
});
const [currentMessage, setCurrentMessage] = React.useState(''); const [currentMessage, setCurrentMessage] = React.useState('');
const sendMessage = (): void => { const sendMessage = (): void => {
if (ready) { if (meshtasticState.ready) {
void connection.sendText( void connection.sendText(
currentMessage, currentMessage,
undefined, isChannel ? undefined : chatIndex,
true, true,
channelIndex--, isChannel ? chatIndex-- : 0,
(id) => { (id) => {
dispatch(ackMessage({ channel: channelIndex--, messageId: id })); dispatch(
ackMessage({
chatIndex: isChannel ? chatIndex-- : chatIndex,
messageId: id,
}),
);
return Promise.resolve(); return Promise.resolve();
}, },
@ -45,7 +60,7 @@ export const MessageBar = ({ channelIndex }: MessageBarProps): JSX.Element => {
type="text" type="text"
minLength={2} minLength={2}
placeholder="Enter Message" placeholder="Enter Message"
disabled={!ready} disabled={!meshtasticState.ready}
value={currentMessage} value={currentMessage}
onChange={(e): void => { onChange={(e): void => {
setCurrentMessage(e.target.value); setCurrentMessage(e.target.value);

161
src/pages/Messages/index.tsx

@ -1,27 +1,30 @@
import React from 'react'; import React from 'react';
import { m } from 'framer-motion'; import { FiHash, FiMessageCircle } from 'react-icons/fi';
import { FiHash, FiMessageCircle, FiSettings } from 'react-icons/fi';
import { MdPublic } from 'react-icons/md';
import TimeAgo from 'timeago-react';
import { Layout } from '@app/components/layout'; import { Layout } from '@app/components/layout';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import { Hashicon } from '@emeraldpay/hashicon-react';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { IconButton, Tooltip } from '@meshtastic/components'; import { IconButton } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
import { ChannelChat } from './ChannelChat';
import { DmChat } from './DmChat';
import { Message } from './Message'; import { Message } from './Message';
import { MessageBar } from './MessageBar'; import { MessageBar } from './MessageBar';
export const Messages = (): JSX.Element => { export const Messages = (): JSX.Element => {
const [selectedChatIndex, setSelectedChatIndex] = React.useState<number>(0);
const chatRef = React.useRef<HTMLDivElement>(null);
const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware,
).myNodeNum;
const nodes = useAppSelector((state) => state.meshtastic.nodes); const nodes = useAppSelector((state) => state.meshtastic.nodes);
const chats = useAppSelector((state) => state.meshtastic.chats);
const channels = useAppSelector( const channels = useAppSelector(
(state) => state.meshtastic.radio.channels, (state) => state.meshtastic.radio.channels,
).filter((ch) => ch.channel.role !== Protobuf.Channel_Role.DISABLED); ).filter((ch) => ch.role !== Protobuf.Channel_Role.DISABLED);
const [channelIndex, setChannelIndex] = React.useState(0);
const chatRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => { React.useEffect(() => {
if (chatRef.current) { if (chatRef.current) {
@ -35,92 +38,29 @@ export const Messages = (): JSX.Element => {
icon={<FiMessageCircle />} icon={<FiMessageCircle />}
sidebarContents={ sidebarContents={
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{nodes.map((node) => ( {nodes
<SidebarItem .filter((node) => node.number !== myNodeNum)
key={node.number} .map((node) => (
selected={false} <DmChat
setSelected={(): void => { key={node.number}
void Promise.resolve(); node={node}
}} selectedIndex={selectedChatIndex}
actions={<IconButton icon={<FiSettings />} />} setSelectedIndex={setSelectedChatIndex}
> />
<div className="flex dark:text-white"> ))}
<div className="m-auto"> {nodes.length !== 0 && channels.length !== 0 && (
<Hashicon value={node.number.toString()} size={32} /> <div className="mx-2 rounded-md border-2 border-gray-300 dark:border-gray-600" />
</div> )}
</div> {channels
<div className="my-auto mr-auto font-semibold dark:text-white"> .filter((channel) => channel.settings?.name !== 'admin')
{node.user?.longName ?? 'Unknown'} .map((channel) => (
</div> <ChannelChat
</SidebarItem> key={channel.index}
))} channel={channel}
<div className="mx-2 rounded-md border-2 border-gray-300 dark:border-gray-600" /> selectedIndex={selectedChatIndex}
{channels.map((channel) => ( setSelectedIndex={setSelectedChatIndex}
<SidebarItem />
key={channel.channel.index} ))}
selected={channelIndex === channel.channel.index}
setSelected={(): void => {
setChannelIndex(channel.channel.index);
}}
actions={<IconButton icon={<FiSettings />} />}
>
<div className="flex h-8 w-8 rounded-full bg-gray-200 dark:bg-primaryDark dark:text-white">
<div className="m-auto">
{channel.channel.role === Protobuf.Channel_Role.PRIMARY ? (
<MdPublic />
) : (
<p>
{channel.channel.settings?.name.length
? channel.channel.settings.name
.substring(0, 3)
.toUpperCase()
: `CH: ${channel.channel.index}`}
</p>
)}
</div>
</div>
{channel.messages.length ? (
<>
<div className="mx-2 flex h-8">
{[
...new Set(
channel.messages.flatMap(({ message }) => [
message.packet.from,
]),
),
]
.sort()
.map((nodeId) => {
return (
<Tooltip
key={nodeId}
content={
nodes.find((node) => node.number === nodeId)?.user
?.longName ?? 'UNK'
}
>
<div className="flex h-full">
<m.div
whileHover={{ scale: 1.1 }}
className="my-auto -ml-2"
>
<Hashicon value={nodeId.toString()} size={20} />
</m.div>
</div>
</Tooltip>
);
})}
</div>
<TimeAgo
className="my-auto ml-auto text-xs font-semibold dark:text-gray-400"
datetime={channel.lastChatInterraction}
/>
</>
) : (
<div className="my-auto dark:text-white">No messages</div>
)}
</SidebarItem>
))}
</div> </div>
} }
> >
@ -128,14 +68,13 @@ export const Messages = (): JSX.Element => {
<div className="flex w-full justify-between border-b border-gray-300 px-2 dark:border-gray-600 dark:text-gray-300"> <div className="flex w-full justify-between border-b border-gray-300 px-2 dark:border-gray-600 dark:text-gray-300">
<div className="my-auto flex gap-2 py-2 text-sm"> <div className="my-auto flex gap-2 py-2 text-sm">
<IconButton icon={<FiHash className="h-4 w-4" />} /> <IconButton icon={<FiHash className="h-4 w-4" />} />
<div className="my-auto"> {/* <div className="my-auto">
{channels[channelIndex]?.channel.settings?.name.length {channels[channelIndex]?.settings?.name.length
? channels[channelIndex]?.channel.settings?.name ? channels[channelIndex]?.settings?.name
: channels[channelIndex]?.channel.role === : channels[channelIndex]?.role === Protobuf.Channel_Role.PRIMARY
Protobuf.Channel_Role.PRIMARY
? 'Primary' ? 'Primary'
: `Channel: ${channels[channelIndex]?.channel.index}`} : `Channel: ${channels[channelIndex]?.index}`}
</div> </div> */}
</div> </div>
</div> </div>
<div <div
@ -143,7 +82,7 @@ export const Messages = (): JSX.Element => {
className="flex flex-grow flex-col space-y-2 overflow-y-auto border-b border-gray-300 bg-white pb-6 dark:border-gray-600 dark:bg-secondaryDark" className="flex flex-grow flex-col space-y-2 overflow-y-auto border-b border-gray-300 bg-white pb-6 dark:border-gray-600 dark:bg-secondaryDark"
> >
<div className="mt-auto"> <div className="mt-auto">
{channels[channelIndex]?.messages.map((message, index) => ( {chats[selectedChatIndex]?.messages.map((message, index) => (
<Message <Message
key={index} key={index}
message={message.message.data} message={message.message.data}
@ -152,17 +91,19 @@ export const Messages = (): JSX.Element => {
lastMsgSameUser={ lastMsgSameUser={
index === 0 index === 0
? false ? false
: channels[channelIndex]?.messages[index - 1].message.packet : chats[selectedChatIndex].messages[index - 1].message
.from === message.message.packet.from .packet.from === message.message.packet.from
} }
sender={nodes.find( sender={nodes.find((node) => {
(node) => node.number === message.message.packet.from, console.log(message);
)}
return node.number === message.message.packet.from;
})}
/> />
))} ))}
</div> </div>
</div> </div>
<MessageBar channelIndex={channelIndex} /> <MessageBar chatIndex={selectedChatIndex} />
</div> </div>
</Layout> </Layout>
); );

106
src/pages/Nodes/NodeCard.tsx

@ -52,59 +52,61 @@ export const NodeCard = ({
); );
}, [node.currentPosition]); }, [node.currentPosition]);
return ( return (
<SidebarItem <>
selected={selected} <SidebarItem
setSelected={setSelected} selected={selected}
actions={ setSelected={setSelected}
<> actions={
<IconButton <>
disabled={PositionConfidence === 'none'} <IconButton
onClick={(e): void => { disabled={PositionConfidence === 'none'}
e.stopPropagation(); onClick={(e): void => {
setSelected(); e.stopPropagation();
if (PositionConfidence !== 'none' && node.currentPosition) { setSelected();
map?.flyTo({ if (PositionConfidence !== 'none' && node.currentPosition) {
center: new mapbox.LngLat( map?.flyTo({
node.currentPosition.longitudeI / 1e7, center: new mapbox.LngLat(
node.currentPosition.latitudeI / 1e7, node.currentPosition.longitudeI / 1e7,
), node.currentPosition.latitudeI / 1e7,
zoom: 16, ),
}); zoom: 16,
});
}
}}
icon={
PositionConfidence === 'high' ? (
<MdGpsFixed />
) : PositionConfidence === 'low' ? (
<MdGpsNotFixed />
) : (
<MdGpsOff />
)
} }
}} />
icon={ <IconButton
PositionConfidence === 'high' ? ( onClick={(e): void => {
<MdGpsFixed /> e.stopPropagation();
) : PositionConfidence === 'low' ? ( setInfoOpen(true);
<MdGpsNotFixed /> }}
) : ( icon={<FiAlignLeft />}
<MdGpsOff /> />
) </>
} }
/> >
<IconButton <div className="flex dark:text-white">
onClick={(e): void => { <div className="m-auto">
e.stopPropagation(); <Hashicon value={node.number.toString()} size={32} />
setInfoOpen(true); </div>
}} </div>
icon={<FiAlignLeft />} <div className="my-auto mr-auto text-xs font-semibold dark:text-gray-400">
/> {node.lastHeard.getTime()
</> ? node.lastHeard.toLocaleTimeString(undefined, {
} hour: '2-digit',
> minute: '2-digit',
<div className="flex dark:text-white"> })
<div className="m-auto"> : 'Never'}
<Hashicon value={node.number.toString()} size={32} />
</div> </div>
</div> </SidebarItem>
<div className="my-auto mr-auto text-xs font-semibold dark:text-gray-400">
{node.lastHeard.getTime()
? node.lastHeard.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
})
: 'Never'}
</div>
<SidebarOverlay <SidebarOverlay
title={`Node ${node.user?.longName ?? 'UNK'} `} title={`Node ${node.user?.longName ?? 'UNK'} `}
open={infoOpen} open={infoOpen}
@ -134,6 +136,6 @@ export const NodeCard = ({
</> </>
</CollapsibleSection> </CollapsibleSection>
</SidebarOverlay> </SidebarOverlay>
</SidebarItem> </>
); );
}; };

Loading…
Cancel
Save