|
|
|
@ -12,34 +12,55 @@ import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react"; |
|
|
|
import { useCallback, useDeferredValue, useMemo, useState } from "react"; |
|
|
|
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx"; |
|
|
|
import { cn } from "@core/utils/cn.ts"; |
|
|
|
import { MessageState, MessageType, useMessageStore } from "@core/stores/messageStore/index.ts"; |
|
|
|
import { |
|
|
|
MessageState, |
|
|
|
MessageType, |
|
|
|
useMessageStore, |
|
|
|
} from "@core/stores/messageStore/index.ts"; |
|
|
|
import { useSidebar } from "@core/stores/sidebarStore.tsx"; |
|
|
|
import { Input } from "@components/UI/Input.tsx"; |
|
|
|
|
|
|
|
type NodeInfoWithUnread = Protobuf.Mesh.NodeInfo & { unreadCount: number }; |
|
|
|
|
|
|
|
export const MessagesPage = () => { |
|
|
|
const { channels, getNodes, getNode, hasNodeError, unreadCounts, resetUnread, connection } = useDevice(); |
|
|
|
const { getMyNodeNum, getMessages, setActiveChat, chatType, activeChat, setChatType, setMessageState, } = useMessageStore() |
|
|
|
const { |
|
|
|
channels, |
|
|
|
getNodes, |
|
|
|
getNode, |
|
|
|
hasNodeError, |
|
|
|
unreadCounts, |
|
|
|
resetUnread, |
|
|
|
connection, |
|
|
|
} = useDevice(); |
|
|
|
const { |
|
|
|
getMyNodeNum, |
|
|
|
getMessages, |
|
|
|
setActiveChat, |
|
|
|
chatType, |
|
|
|
activeChat, |
|
|
|
setChatType, |
|
|
|
setMessageState, |
|
|
|
} = useMessageStore(); |
|
|
|
const { toast } = useToast(); |
|
|
|
const { isCollapsed } = useSidebar() |
|
|
|
const { isCollapsed } = useSidebar(); |
|
|
|
const [searchTerm, setSearchTerm] = useState<string>(""); |
|
|
|
const deferredSearch = useDeferredValue(searchTerm); |
|
|
|
|
|
|
|
const filteredNodes = (): NodeInfoWithUnread[] => { |
|
|
|
const lowerCaseSearchTerm = deferredSearch.toLowerCase(); |
|
|
|
|
|
|
|
return getNodes(node => { |
|
|
|
const longName = node.user?.longName?.toLowerCase() ?? ''; |
|
|
|
const shortName = node.user?.shortName?.toLowerCase() ?? ''; |
|
|
|
return longName.includes(lowerCaseSearchTerm) || shortName.includes(lowerCaseSearchTerm) |
|
|
|
return getNodes((node) => { |
|
|
|
const longName = node.user?.longName?.toLowerCase() ?? ""; |
|
|
|
const shortName = node.user?.shortName?.toLowerCase() ?? ""; |
|
|
|
return longName.includes(lowerCaseSearchTerm) || |
|
|
|
shortName.includes(lowerCaseSearchTerm); |
|
|
|
}) |
|
|
|
.map((node) => ({ |
|
|
|
...node, |
|
|
|
unreadCount: unreadCounts.get(node.num) ?? 0, |
|
|
|
})) |
|
|
|
.sort((a, b) => b.unreadCount - a.unreadCount); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const allChannels = Array.from(channels.values()); |
|
|
|
const filteredChannels = allChannels.filter( |
|
|
|
@ -55,19 +76,39 @@ export const MessagesPage = () => { |
|
|
|
const isDirect = chatType === MessageType.Direct; |
|
|
|
const toValue = isDirect ? activeChat : MessageType.Broadcast; |
|
|
|
|
|
|
|
const channelValue = isDirect ? Types.ChannelNumber.Primary : activeChat ?? 0; |
|
|
|
const channelValue = isDirect |
|
|
|
? Types.ChannelNumber.Primary |
|
|
|
: activeChat ?? 0; |
|
|
|
|
|
|
|
console.log(`Sending message: "${message}" to: ${toValue}, channel: ${channelValue}, type: ${chatType}`); |
|
|
|
console.log( |
|
|
|
`Sending message: "${message}" to: ${toValue}, channel: ${channelValue}, type: ${chatType}`, |
|
|
|
); |
|
|
|
|
|
|
|
let messageId: number | undefined; |
|
|
|
|
|
|
|
try { |
|
|
|
messageId = await connection?.sendText(message, toValue, true, channelValue); |
|
|
|
messageId = await connection?.sendText( |
|
|
|
message, |
|
|
|
toValue, |
|
|
|
true, |
|
|
|
channelValue, |
|
|
|
); |
|
|
|
if (messageId !== undefined) { |
|
|
|
if (chatType === MessageType.Broadcast) { |
|
|
|
setMessageState({ type: chatType, channelId: channelValue, messageId, newState: MessageState.Ack }); |
|
|
|
setMessageState({ |
|
|
|
type: chatType, |
|
|
|
channelId: channelValue, |
|
|
|
messageId, |
|
|
|
newState: MessageState.Ack, |
|
|
|
}); |
|
|
|
} else { |
|
|
|
setMessageState({ type: chatType, nodeA: getMyNodeNum(), nodeB: activeChat, messageId, newState: MessageState.Ack }); |
|
|
|
setMessageState({ |
|
|
|
type: chatType, |
|
|
|
nodeA: getMyNodeNum(), |
|
|
|
nodeB: activeChat, |
|
|
|
messageId, |
|
|
|
newState: MessageState.Ack, |
|
|
|
}); |
|
|
|
} |
|
|
|
} else { |
|
|
|
console.warn("sendText completed but messageId is undefined"); |
|
|
|
@ -78,10 +119,21 @@ export const MessagesPage = () => { |
|
|
|
// Note: messageId might be undefined here if the error occurred before it was assigned
|
|
|
|
if (chatType === MessageType.Broadcast) { |
|
|
|
const failedId = messageId ?? `failed-${Date.now()}`; |
|
|
|
setMessageState({ type: chatType, channelId: channelValue, messageId: failedId, newState: MessageState.Failed }); |
|
|
|
setMessageState({ |
|
|
|
type: chatType, |
|
|
|
channelId: channelValue, |
|
|
|
messageId: failedId, |
|
|
|
newState: MessageState.Failed, |
|
|
|
}); |
|
|
|
} else { // MessageType.Direct
|
|
|
|
const failedId = messageId ?? `failed-${Date.now()}`; |
|
|
|
setMessageState({ type: chatType, nodeA: getMyNodeNum(), nodeB: activeChat, messageId: failedId, newState: MessageState.Failed }); |
|
|
|
setMessageState({ |
|
|
|
type: chatType, |
|
|
|
nodeA: getMyNodeNum(), |
|
|
|
nodeB: activeChat, |
|
|
|
messageId: failedId, |
|
|
|
newState: MessageState.Failed, |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
}, [activeChat, chatType, connection, getMyNodeNum, setMessageState]); |
|
|
|
@ -100,7 +152,11 @@ export const MessagesPage = () => { |
|
|
|
case MessageType.Direct: |
|
|
|
return ( |
|
|
|
<ChannelChat |
|
|
|
messages={getMessages({ type: MessageType.Direct, nodeA: getMyNodeNum(), nodeB: activeChat })} |
|
|
|
messages={getMessages({ |
|
|
|
type: MessageType.Direct, |
|
|
|
nodeA: getMyNodeNum(), |
|
|
|
nodeB: activeChat, |
|
|
|
})} |
|
|
|
/> |
|
|
|
); |
|
|
|
default: |
|
|
|
@ -110,7 +166,7 @@ export const MessagesPage = () => { |
|
|
|
</div> |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const leftSidebar = useMemo(() => ( |
|
|
|
<Sidebar> |
|
|
|
@ -119,72 +175,108 @@ export const MessagesPage = () => { |
|
|
|
<SidebarButton |
|
|
|
key={channel.index} |
|
|
|
count={unreadCounts.get(channel.index)} |
|
|
|
label={channel.settings?.name || (channel.index === 0 ? "Primary" : `Ch ${channel.index}`)} |
|
|
|
active={activeChat === channel.index && chatType === MessageType.Broadcast} |
|
|
|
label={channel.settings?.name || |
|
|
|
(channel.index === 0 ? "Primary" : `Ch ${channel.index}`)} |
|
|
|
active={activeChat === channel.index && |
|
|
|
chatType === MessageType.Broadcast} |
|
|
|
onClick={() => { |
|
|
|
setChatType(MessageType.Broadcast); |
|
|
|
setActiveChat(channel.index); |
|
|
|
resetUnread(channel.index); |
|
|
|
}} |
|
|
|
> |
|
|
|
<HashIcon size={16} className={cn(isCollapsed ? "mr-0 mt-2" : "mr-2")} /> |
|
|
|
<HashIcon |
|
|
|
size={16} |
|
|
|
className={cn(isCollapsed ? "mr-0 mt-2" : "mr-2")} |
|
|
|
/> |
|
|
|
</SidebarButton> |
|
|
|
))} |
|
|
|
</SidebarSection> |
|
|
|
</Sidebar> |
|
|
|
), [filteredChannels, unreadCounts, activeChat, chatType, isCollapsed, setActiveChat, setChatType, resetUnread]); |
|
|
|
|
|
|
|
const rightSidebar = useMemo(() => ( |
|
|
|
<SidebarSection label="" className="px-0 flex flex-col h-full overflow-y-auto"> |
|
|
|
<label className="p-2 block"> |
|
|
|
<Input |
|
|
|
type="text" |
|
|
|
placeholder="Search nodes..." |
|
|
|
value={searchTerm} |
|
|
|
onChange={(e) => setSearchTerm(e.target.value)} |
|
|
|
showClearButton={!!searchTerm} |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<div className={cn( |
|
|
|
"flex flex-col h-full flex-1 overflow-y-auto gap-2.5 pt-1 ", |
|
|
|
)}> |
|
|
|
{filteredNodes()?.map((node) => ( |
|
|
|
<SidebarButton |
|
|
|
key={node.num} |
|
|
|
preventCollapse={true} |
|
|
|
label={node.user?.longName ?? `UNK`} |
|
|
|
count={node.unreadCount > 0 ? node.unreadCount : undefined} |
|
|
|
active={activeChat === node.num && chatType === MessageType.Direct} |
|
|
|
onClick={() => { |
|
|
|
setChatType(MessageType.Direct); |
|
|
|
setActiveChat(node.num); |
|
|
|
resetUnread(node.num); |
|
|
|
}}> |
|
|
|
<Avatar |
|
|
|
text={node.user?.shortName ?? "UNK"} |
|
|
|
className={cn(hasNodeError(node.num) && "text-red-500")} |
|
|
|
showError={hasNodeError(node.num)} |
|
|
|
size="sm" |
|
|
|
/> |
|
|
|
</SidebarButton> |
|
|
|
))} |
|
|
|
</div> |
|
|
|
</SidebarSection> |
|
|
|
), [filteredNodes, searchTerm, activeChat, chatType, setActiveChat, setChatType, resetUnread, hasNodeError]); |
|
|
|
), [ |
|
|
|
filteredChannels, |
|
|
|
unreadCounts, |
|
|
|
activeChat, |
|
|
|
chatType, |
|
|
|
isCollapsed, |
|
|
|
setActiveChat, |
|
|
|
setChatType, |
|
|
|
resetUnread, |
|
|
|
]); |
|
|
|
|
|
|
|
const rightSidebar = useMemo( |
|
|
|
() => ( |
|
|
|
<SidebarSection |
|
|
|
label="" |
|
|
|
className="px-0 flex flex-col h-full overflow-y-auto" |
|
|
|
> |
|
|
|
<label className="p-2 block"> |
|
|
|
<Input |
|
|
|
type="text" |
|
|
|
placeholder="Search nodes..." |
|
|
|
value={searchTerm} |
|
|
|
onChange={(e) => setSearchTerm(e.target.value)} |
|
|
|
showClearButton={!!searchTerm} |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<div |
|
|
|
className={cn( |
|
|
|
"flex flex-col h-full flex-1 overflow-y-auto gap-2.5 pt-1 ", |
|
|
|
)} |
|
|
|
> |
|
|
|
{filteredNodes()?.map((node) => ( |
|
|
|
<SidebarButton |
|
|
|
key={node.num} |
|
|
|
// Default value for preventCollapse is false, hence ignore linting error:
|
|
|
|
// deno-lint-ignore jsx-boolean-value
|
|
|
|
preventCollapse={true} |
|
|
|
label={node.user?.longName ?? `UNK`} |
|
|
|
count={node.unreadCount > 0 ? node.unreadCount : undefined} |
|
|
|
active={activeChat === node.num && |
|
|
|
chatType === MessageType.Direct} |
|
|
|
onClick={() => { |
|
|
|
setChatType(MessageType.Direct); |
|
|
|
setActiveChat(node.num); |
|
|
|
resetUnread(node.num); |
|
|
|
}} |
|
|
|
> |
|
|
|
<Avatar |
|
|
|
text={node.user?.shortName ?? "UNK"} |
|
|
|
className={cn(hasNodeError(node.num) && "text-red-500")} |
|
|
|
showError={hasNodeError(node.num)} |
|
|
|
size="sm" |
|
|
|
/> |
|
|
|
</SidebarButton> |
|
|
|
))} |
|
|
|
</div> |
|
|
|
</SidebarSection> |
|
|
|
), |
|
|
|
[ |
|
|
|
filteredNodes, |
|
|
|
searchTerm, |
|
|
|
activeChat, |
|
|
|
chatType, |
|
|
|
setActiveChat, |
|
|
|
setChatType, |
|
|
|
resetUnread, |
|
|
|
hasNodeError, |
|
|
|
], |
|
|
|
); |
|
|
|
return ( |
|
|
|
<PageLayout |
|
|
|
label={`Messages: ${isBroadcast && currentChannel |
|
|
|
? getChannelName(currentChannel) |
|
|
|
: isDirect && otherNode |
|
|
|
label={`Messages: ${ |
|
|
|
isBroadcast && currentChannel |
|
|
|
? getChannelName(currentChannel) |
|
|
|
: isDirect && otherNode |
|
|
|
? (otherNode.user?.longName ?? "Unknown") |
|
|
|
: "Select a Chat" |
|
|
|
}`}
|
|
|
|
}`}
|
|
|
|
rightBar={rightSidebar} |
|
|
|
leftBar={leftSidebar} |
|
|
|
actions={isDirect && otherNode |
|
|
|
? [ |
|
|
|
{ |
|
|
|
key: 'encryption', |
|
|
|
key: "encryption", |
|
|
|
icon: otherNode.user?.publicKey?.length ? LockIcon : LockOpenIcon, |
|
|
|
iconClasses: otherNode.user?.publicKey?.length |
|
|
|
? "text-green-600" |
|
|
|
@ -204,19 +296,23 @@ export const MessagesPage = () => { |
|
|
|
{renderChatContent()} |
|
|
|
|
|
|
|
<div className="flex-none dark:bg-slate-900 p-2"> |
|
|
|
{(isBroadcast || isDirect) ? ( |
|
|
|
<MessageInput |
|
|
|
to={isDirect ? activeChat : MessageType.Broadcast} |
|
|
|
onSend={sendText} |
|
|
|
maxBytes={200} |
|
|
|
/> |
|
|
|
) : ( |
|
|
|
<div className="p-4 text-center text-slate-400 italic">Select a chat to send a message.</div> |
|
|
|
)} |
|
|
|
{(isBroadcast || isDirect) |
|
|
|
? ( |
|
|
|
<MessageInput |
|
|
|
to={isDirect ? activeChat : MessageType.Broadcast} |
|
|
|
onSend={sendText} |
|
|
|
maxBytes={200} |
|
|
|
/> |
|
|
|
) |
|
|
|
: ( |
|
|
|
<div className="p-4 text-center text-slate-400 italic"> |
|
|
|
Select a chat to send a message. |
|
|
|
</div> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</PageLayout> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
export default MessagesPage; |
|
|
|
export default MessagesPage; |
|
|
|
|