Browse Source

merge birdpump notif sound, message persistence and notification icon

pull/342/head
AndrewH 2 years ago
parent
commit
5d01b8a12f
  1. BIN
      public/notification.wav
  2. 12
      src/components/DeviceSelector.tsx
  3. 6
      src/components/Sidebar.tsx
  4. 8
      src/components/UI/Sidebar/sidebarButton.tsx
  5. 10
      src/core/stores/appStore.ts
  6. 166
      src/core/stores/deviceStore.ts
  7. 4
      src/core/subscriptions.ts
  8. 17
      src/core/utils/notify.ts
  9. 8
      src/index.css
  10. 5
      src/pages/Messages.tsx

BIN
public/notification.wav

Binary file not shown.

12
src/components/DeviceSelector.tsx

@ -5,12 +5,15 @@ import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { Hashicon } from "@emeraldpay/hashicon-react";
import {
BellIcon,
BellOffIcon,
HomeIcon,
LanguagesIcon,
MoonIcon,
PlusIcon,
SearchIcon,
SunIcon,
TerminalIcon,
} from "lucide-react";
export const DeviceSelector = (): JSX.Element => {
@ -20,6 +23,8 @@ export const DeviceSelector = (): JSX.Element => {
setSelectedDevice,
darkMode,
setDarkMode,
notifications,
setNotifications,
setCommandPaletteOpen,
setConnectDialogOpen,
} = useAppStore();
@ -61,6 +66,13 @@ export const DeviceSelector = (): JSX.Element => {
</ul>
</div>
<div className="flex w-20 flex-col items-center space-y-5 bg-transparent px-5 pb-5">
<button
type="button"
className="transition-all hover:text-accent"
onClick={() => setNotifications(!notifications)}
>
{notifications ? <BellIcon /> : <BellOffIcon />}
</button>
<button
type="button"
className="transition-all hover:text-accent"

6
src/components/Sidebar.tsx

@ -24,7 +24,7 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
const { hardware, nodes, metadata } = useDevice();
const myNode = nodes.get(hardware.myNodeNum);
const myMetadata = metadata.get(0);
const { activePage, setActivePage, setDialogOpen } = useDevice();
const { activePage, setActivePage, setDialogOpen, hasUnread } = useDevice();
interface NavLink {
name: string;
@ -94,7 +94,8 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
</div>
</div>
<SidebarSection label="Navigation">
<SidebarSection label="Navigation">
{pages.map((link) => (
<SidebarButton
key={link.name}
@ -104,6 +105,7 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
setActivePage(link.page);
}}
active={link.page === activePage}
unread={link.name == "Messages" ? hasUnread(null, null) : false}
/>
))}
</SidebarSection>

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

@ -1,11 +1,12 @@
import { Button } from "@components/UI/Button.tsx";
import type { LucideIcon } from "lucide-react";
import {LucideMessageSquareDot, type LucideIcon } from "lucide-react";
export interface SidebarButtonProps {
label: string;
active?: boolean;
Icon?: LucideIcon;
element?: JSX.Element;
unread: boolean;
onClick?: () => void;
}
@ -14,6 +15,7 @@ export const SidebarButton = ({
active,
Icon,
element,
unread,
onClick,
}: SidebarButtonProps): JSX.Element => (
<Button
@ -25,5 +27,9 @@ export const SidebarButton = ({
{Icon && <Icon size={16} />}
{element && element}
{label}
{unread ?
<div className="notifyBadge"><LucideMessageSquareDot size={12} /></div>
: null
}
</Button>
);

10
src/core/stores/appStore.ts

@ -26,6 +26,7 @@ interface AppState {
rasterSources: RasterSource[];
commandPaletteOpen: boolean;
darkMode: boolean;
notifications: boolean;
nodeNumToBeRemoved: number;
accent: AccentColor;
connectDialogOpen: boolean;
@ -39,6 +40,7 @@ interface AppState {
removeDevice: (deviceId: number) => void;
setCommandPaletteOpen: (open: boolean) => void;
setDarkMode: (enabled: boolean) => void;
setNotifications: (enabled: boolean) => void;
setNodeNumToBeRemoved: (nodeNum: number) => void;
setAccent: (color: AccentColor) => void;
setConnectDialogOpen: (open: boolean) => void;
@ -50,6 +52,7 @@ export const useAppStore = create<AppState>()((set) => ({
currentPage: "messages",
rasterSources: [],
commandPaletteOpen: false,
notifications: true,
darkMode:
localStorage.getItem("theme-dark") !== null
? localStorage.getItem("theme-dark") === "true"
@ -106,6 +109,13 @@ export const useAppStore = create<AppState>()((set) => ({
}),
);
},
setNotifications: (enabled: boolean) => {
set(
produce<AppState>((draft) => {
draft.notifications = enabled;
}),
);
},
setNodeNumToBeRemoved: (nodeNum) =>
set((state) => ({
nodeNumToBeRemoved: nodeNum,

166
src/core/stores/deviceStore.ts

@ -9,6 +9,7 @@ export type Page = "messages" | "map" | "config" | "channels" | "nodes";
export interface MessageWithState extends Types.PacketMetadata<string> {
state: MessageState;
unread: boolean;
}
export type MessageState = "ack" | "waiting" | Protobuf.Mesh.Routing_Error;
@ -95,6 +96,8 @@ export interface Device {
setDialogOpen: (dialog: DialogVariant, open: boolean) => void;
processPacket: (data: ProcessPacketParams) => void;
setMessageDraft: (message: string) => void;
hasUnread: (channel: number | null, nodeNum: number | null) => boolean;
markAllRead: (channel: number | null, nodeNum: number | null) => void;
}
export interface DeviceState {
@ -112,7 +115,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
remoteDevices: new Map(),
addDevice: (id: number) => {
set(
set(
produce<DeviceState>((draft) => {
draft.devices.set(id, {
id,
@ -312,10 +315,10 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
},
setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.hardware = hardware;
device.hardware = hardware;
}
}),
);
@ -392,6 +395,30 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
return;
}
device.channels.set(channel.index, channel);
const messageGroup = device.messages["broadcast"];
let msgJson = localStorage.getItem("msg_" + channel.index)
if (msgJson !== null) {
let storedMsgs = JSON.parse(msgJson)
for (let a = 0; a < storedMsgs.length; a++) {
let message = <MessageWithState>storedMsgs[a]
if (channel.index == 0) {
message.unread = false
}
message.rxTime = new Date(message.rxTime)
message.state = "ack"
let messageIndex = message.channel
let messages = messageGroup.get(messageIndex);
if (messages === undefined) {
messages = [message]
} else {
messages.push(message);
}
messageGroup.set(messageIndex, messages)
}
}
}),
);
},
@ -421,6 +448,26 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
return;
}
device.nodes.set(nodeInfo.num, nodeInfo);
const messageGroup = device.messages["direct"];
let msgJson = localStorage.getItem("msg_" + nodeInfo.num)
if (msgJson !== null) {
let storedMsgs = JSON.parse(msgJson)
for (let a = 0; a < storedMsgs.length; a++) {
let message = <MessageWithState>storedMsgs[a]
message.rxTime = new Date(message.rxTime)
message.state = "ack"
let messageIndex = (message.from === device.hardware.myNodeNum) ? message.to : message.from
let messages = messageGroup.get(messageIndex);
if (messages === undefined) {
messages = [message]
} else {
messages.push(message);
}
messageGroup.set(messageIndex, messages)
}
}
}),
);
},
@ -474,9 +521,10 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
);
},
addMessage: (message) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
let device = draft.devices.get(id);
if (!device) {
return;
}
@ -487,14 +535,19 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
? message.to
: message.from
: message.channel;
const messages = messageGroup.get(messageIndex);
let messages = messageGroup.get(messageIndex);
if (messages) {
messages.push(message);
messageGroup.set(messageIndex, messages);
if (message.from !== device.hardware.myNodeNum) {
message.unread = true;
}
if (messages === undefined) {
messages = [message];
} else {
messageGroup.set(messageIndex, [message]);
messages.push(message);
}
messageGroup.set(messageIndex, messages);
localStorage.setItem("msg_" + messageIndex, JSON.stringify(messages))
}),
);
},
@ -513,8 +566,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
addTraceRoute: (traceroute) => {
set(
produce<DeviceState>((draft) => {
console.log("addTraceRoute called");
console.log(traceroute);
//console.log("addTraceRoute called");
//console.log(traceroute);
const device = draft.devices.get(id);
if (!device) {
return;
@ -632,6 +685,94 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
}),
);
},
hasUnread: (channel: number | null, nodeNum: number | null) => {
const device = get().devices.get(id);
if (device == null) {
return false
}
if (channel == null && nodeNum == null) {
let channelsWithMessages = device.messages["broadcast"]
for (let [channelIndex, messages] of channelsWithMessages) {
for (let msg of messages) {
if (msg.unread) {
return true
}
}
}
channelsWithMessages = device.messages["direct"]
for (let [nodeIndex, messages] of channelsWithMessages) {
for (let msg of messages) {
if (msg.unread) {
return true
}
}
}
}
if (channel !== null) {
const channelsWithMessages = device.messages["broadcast"]
let messages = channelsWithMessages.get(channel)
if (messages == null) {
return false
}
for (let msg of messages) {
if (msg.unread) {
return true
}
}
}
if (nodeNum !== null) {
const channelsWithMessages = device.messages["direct"]
let messages = channelsWithMessages.get(nodeNum)
if (messages == null) {
return false
}
for (let msg of messages) {
if (msg.unread) {
return true
}
}
}
return false
},
markAllRead: (channel: number | null, nodeNum: number | null) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device == null) {
return
}
if (channel !== null) {
const channelsWithMessages = device.messages["broadcast"]
let messages = channelsWithMessages.get(channel)
if (messages == null) {
return
}
for (let msg of messages) {
msg.unread = false
}
channelsWithMessages.set(channel, messages);
localStorage.setItem("msg_" + channel, JSON.stringify(messages))
}
if (nodeNum !== null) {
const directsWithMessages = device.messages["direct"]
let messages = directsWithMessages.get(nodeNum)
if (messages == null) {
return
}
for (let msg of messages) {
msg.unread = false
}
directsWithMessages.set(nodeNum, messages);
localStorage.setItem("msg_" + nodeNum, JSON.stringify(messages))
}
})
)
}
});
}),
);
@ -654,6 +795,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
getDevices: () => Array.from(get().devices.values()),
getDevice: (id) => get().devices.get(id),
}));
export const DeviceContext = createContext<Device | undefined>(undefined);

4
src/core/subscriptions.ts

@ -1,5 +1,6 @@
import type { Device } from "@core/stores/deviceStore.ts";
import { Protobuf, type Types } from "@meshtastic/js";
import { playNotificationSound } from "./utils/notify";
export const subscribeAll = (
device: Device,
@ -84,6 +85,9 @@ export const subscribeAll = (
...messagePacket,
state: messagePacket.from !== myNodeNum ? "ack" : "waiting",
});
if (messagePacket.from !== myNodeNum) {
playNotificationSound();
}
});
connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => {

17
src/core/utils/notify.ts

@ -0,0 +1,17 @@
import { useAppStore } from "@core/stores/appStore.ts";
const notificationSound = new Audio("/notification.wav"); //change sound if needed
let isPlaying = false;
export const playNotificationSound = () => {
const { notifications } = useAppStore.getState();
if (notifications && !isPlaying) {
isPlaying = true;
notificationSound.play();
notificationSound.onended = () => {
isPlaying = false;
};
}
};

8
src/index.css

@ -100,3 +100,11 @@ img {
-drag: none;
-webkit-user-drag: none;
}
.notifyBadge {
background-color: #f25555;
border-radius: 50%;
padding:5px;
color: #ffffff;
font-weight: bold;
}

5
src/pages/Messages.tsx

@ -15,6 +15,7 @@ import { useState } from "react";
export const MessagesPage = (): JSX.Element => {
const { channels, nodes, hardware, messages, traceroutes, connection } =
useDevice();
const { hasUnread, markAllRead } = useDevice();
const [chatType, setChatType] =
useState<Types.PacketDestination>("broadcast");
const [activeChat, setActiveChat] = useState<number>(
@ -48,10 +49,12 @@ export const MessagesPage = (): JSX.Element => {
}
active={activeChat === channel.index}
onClick={() => {
markAllRead(channel.index, null)
setChatType("broadcast");
setActiveChat(channel.index);
}}
element={<HashIcon size={16} className="mr-2" />}
unread={hasUnread(channel.index, null)}
/>
))}
</SidebarSection>
@ -62,10 +65,12 @@ export const MessagesPage = (): JSX.Element => {
label={node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`}
active={activeChat === node.num}
onClick={() => {
markAllRead(null, node.num)
setChatType("direct");
setActiveChat(node.num);
}}
element={<Hashicon size={20} value={node.num.toString()} />}
unread={hasUnread(null, node.num)}
/>
))}
</SidebarSection>

Loading…
Cancel
Save