This commit is contained in:
Sacha Weatherstone
2022-02-07 00:00:55 +11:00
parent e43b04990b
commit 8d184578f0
109 changed files with 3495 additions and 3632 deletions
+81
View File
@@ -0,0 +1,81 @@
import type React from 'react';
import type { Node } from '@app/core/slices/meshtasticSlice';
import { Tooltip } from '@meshtastic/components';
// eslint-disable-next-line import/no-unresolved
import skypack_hashicon from '@skypack/@emeraldpay/hashicon-react';
const Hashicon = skypack_hashicon.Hashicon;
export interface MessageProps {
lastMsgSameUser: boolean;
message: string;
ack: boolean;
rxTime: Date;
sender?: Node;
}
export const Message = ({
lastMsgSameUser,
message,
ack,
rxTime,
sender,
}: MessageProps): JSX.Element => {
return (
<div className="group mb-3 hover:bg-gray-200 dark:hover:bg-primaryDark">
{lastMsgSameUser ? (
<div
className={`mx-6 -mt-3 flex gap-2 ${lastMsgSameUser ? '' : 'py-1'}`}
>
<div className="flex">
<Tooltip content={rxTime.toString()}>
<div className="my-auto ml-auto w-8 pt-1 text-xs text-transparent dark:group-hover:text-gray-400">
{rxTime
.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
})
.replace('AM', '')
.replace('PM', '')}
</div>
</Tooltip>
</div>
<div
className={`my-auto dark:text-gray-300 ${
ack ? '' : 'animate-pulse dark:text-gray-500'
}`}
>
{message}
</div>
</div>
) : (
<div className="mx-6 flex gap-2">
<div className="my-auto w-8">
<Hashicon value={(sender?.number ?? 0).toString()} size={32} />
</div>
<div>
<div className="flex gap-2">
<div className="cursor-default text-sm font-semibold hover:underline dark:text-white">
{sender?.user?.longName ?? 'UNK'}
</div>
<div className="my-auto text-xs dark:text-gray-400">
{rxTime.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
})}
</div>
</div>
<div
className={`dark:text-gray-300 ${
ack ? '' : 'animate-pulse dark:text-gray-400'
}`}
>
{message}
</div>
</div>
</div>
)}
</div>
);
};
+58
View File
@@ -0,0 +1,58 @@
import React from 'react';
import { connection } from '@core/connection';
import { ackMessage } from '@core/slices/meshtasticSlice';
import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector';
import { Input } from '@meshtastic/components';
export interface MessageBarProps {
channelIndex: number;
}
export const MessageBar = ({ channelIndex }: MessageBarProps): JSX.Element => {
const dispatch = useAppDispatch();
const ready = useAppSelector((state) => state.meshtastic.ready);
const [currentMessage, setCurrentMessage] = React.useState('');
const sendMessage = (): void => {
if (ready) {
void connection.sendText(
currentMessage,
undefined,
true,
channelIndex--,
(id) => {
dispatch(ackMessage({ channel: channelIndex--, messageId: id }));
return Promise.resolve();
},
);
setCurrentMessage('');
}
};
return (
<div className="mx-auto flex w-full space-x-2 bg-gray-50 p-4 text-gray-500 dark:bg-transparent dark:text-gray-400">
<div className="mx-auto flex w-full max-w-4xl">
<form
className="flex w-full space-x-2"
onSubmit={(e): void => {
e.preventDefault();
sendMessage();
}}
>
<Input
type="text"
minLength={2}
placeholder="Enter Message"
disabled={!ready}
value={currentMessage}
onChange={(e): void => {
setCurrentMessage(e.target.value);
}}
/>
</form>
</div>
</div>
);
};
+152
View File
@@ -0,0 +1,152 @@
import React from 'react';
import { m } from 'framer-motion';
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 { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import { useAppSelector } from '@hooks/useAppSelector';
import { IconButton, Tooltip } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
// eslint-disable-next-line import/no-unresolved
import skypack_hashicon from '@skypack/@emeraldpay/hashicon-react';
import { Message } from './Message';
import { MessageBar } from './MessageBar';
const Hashicon = skypack_hashicon.Hashicon;
export const Messages = (): JSX.Element => {
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const channels = useAppSelector(
(state) => state.meshtastic.radio.channels,
).filter((ch) => ch.channel.role !== Protobuf.Channel_Role.DISABLED);
const [channelIndex, setChannelIndex] = React.useState(0);
const chatRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (chatRef.current) {
chatRef.current.scrollTop = chatRef.current.scrollHeight;
}
}, [channels]);
return (
<Layout
title="Message Groups"
icon={<FiMessageCircle />}
sidebarContents={
<div className="flex flex-col gap-2">
{channels.map((channel) => (
<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 className="flex w-full flex-col">
<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 py-2 text-sm">
<FiHash className="my-auto mr-1 h-4 w-4" />
<div>
{channels[channelIndex]?.channel.settings?.name.length
? channels[channelIndex]?.channel.settings?.name
: channels[channelIndex]?.channel.role ===
Protobuf.Channel_Role.PRIMARY
? 'Primary'
: `Channel: ${channels[channelIndex]?.channel.index}`}
</div>
</div>
</div>
<div
ref={chatRef}
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">
{channels[channelIndex]?.messages.map((message, index) => (
<Message
key={index}
message={message.message.data}
ack={message.ack}
rxTime={message.received}
lastMsgSameUser={
index === 0
? false
: channels[channelIndex]?.messages[index - 1].message.packet
.from === message.message.packet.from
}
sender={nodes.find(
(node) => node.number === message.message.packet.from,
)}
/>
))}
</div>
</div>
<MessageBar channelIndex={channelIndex} />
</div>
</Layout>
);
};