mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-06 16:53:38 +02:00
Overhaul frontend organization and pause message polling during repeater operations
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export { useRepeaterMode, type UseRepeaterModeResult, formatDuration, formatTelemetry, formatNeighbors, formatAcl } from './useRepeaterMode';
|
||||
export { useUnreadCounts, type UseUnreadCountsResult } from './useUnreadCounts';
|
||||
export { useConversationMessages, type UseConversationMessagesResult, getMessageContentKey } from './useConversationMessages';
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { api } from '../api';
|
||||
import type { Conversation, Message } from '../types';
|
||||
|
||||
const MESSAGE_PAGE_SIZE = 200;
|
||||
|
||||
// Generate a key for deduplicating messages by content
|
||||
export function getMessageContentKey(msg: Message): string {
|
||||
return `${msg.type}-${msg.conversation_key}-${msg.text}-${msg.sender_timestamp}`;
|
||||
}
|
||||
|
||||
export interface UseConversationMessagesResult {
|
||||
messages: Message[];
|
||||
messagesLoading: boolean;
|
||||
loadingOlder: boolean;
|
||||
hasOlderMessages: boolean;
|
||||
setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
|
||||
fetchMessages: (showLoading?: boolean) => Promise<void>;
|
||||
fetchOlderMessages: () => Promise<void>;
|
||||
addMessageIfNew: (msg: Message) => boolean;
|
||||
updateMessageAck: (messageId: number, ackCount: number) => void;
|
||||
}
|
||||
|
||||
export function useConversationMessages(
|
||||
activeConversation: Conversation | null
|
||||
): UseConversationMessagesResult {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [messagesLoading, setMessagesLoading] = useState(false);
|
||||
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||
const [hasOlderMessages, setHasOlderMessages] = useState(false);
|
||||
|
||||
// Track seen message content for deduplication
|
||||
const seenMessageContent = useRef<Set<string>>(new Set());
|
||||
|
||||
// Fetch messages for active conversation
|
||||
const fetchMessages = useCallback(async (showLoading = false) => {
|
||||
if (!activeConversation || activeConversation.type === 'raw') {
|
||||
setMessages([]);
|
||||
setHasOlderMessages(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
setMessagesLoading(true);
|
||||
}
|
||||
try {
|
||||
const data = await api.getMessages({
|
||||
type: activeConversation.type === 'channel' ? 'CHAN' : 'PRIV',
|
||||
conversation_key: activeConversation.id,
|
||||
limit: MESSAGE_PAGE_SIZE,
|
||||
});
|
||||
setMessages(data);
|
||||
// Track seen content for new messages
|
||||
seenMessageContent.current.clear();
|
||||
for (const msg of data) {
|
||||
seenMessageContent.current.add(getMessageContentKey(msg));
|
||||
}
|
||||
// If we got a full page, there might be more
|
||||
setHasOlderMessages(data.length >= MESSAGE_PAGE_SIZE);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch messages:', err);
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
setMessagesLoading(false);
|
||||
}
|
||||
}
|
||||
}, [activeConversation]);
|
||||
|
||||
// Fetch older messages (pagination)
|
||||
const fetchOlderMessages = useCallback(async () => {
|
||||
if (!activeConversation || activeConversation.type === 'raw' || loadingOlder || !hasOlderMessages) return;
|
||||
|
||||
setLoadingOlder(true);
|
||||
try {
|
||||
const data = await api.getMessages({
|
||||
type: activeConversation.type === 'channel' ? 'CHAN' : 'PRIV',
|
||||
conversation_key: activeConversation.id,
|
||||
limit: MESSAGE_PAGE_SIZE,
|
||||
offset: messages.length,
|
||||
});
|
||||
|
||||
if (data.length > 0) {
|
||||
// Prepend older messages (they come sorted DESC, so older are at the end)
|
||||
setMessages(prev => [...prev, ...data]);
|
||||
// Track seen content
|
||||
for (const msg of data) {
|
||||
seenMessageContent.current.add(getMessageContentKey(msg));
|
||||
}
|
||||
}
|
||||
// If we got less than a full page, no more messages
|
||||
setHasOlderMessages(data.length >= MESSAGE_PAGE_SIZE);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch older messages:', err);
|
||||
} finally {
|
||||
setLoadingOlder(false);
|
||||
}
|
||||
}, [activeConversation, loadingOlder, hasOlderMessages, messages.length]);
|
||||
|
||||
// Fetch messages when conversation changes
|
||||
useEffect(() => {
|
||||
fetchMessages(true);
|
||||
}, [fetchMessages]);
|
||||
|
||||
// Add a message if it's new (deduplication)
|
||||
// Returns true if the message was added, false if it was a duplicate
|
||||
const addMessageIfNew = useCallback((msg: Message): boolean => {
|
||||
const contentKey = getMessageContentKey(msg);
|
||||
if (seenMessageContent.current.has(contentKey)) {
|
||||
console.debug('Duplicate message content ignored:', contentKey.slice(0, 50));
|
||||
return false;
|
||||
}
|
||||
seenMessageContent.current.add(contentKey);
|
||||
|
||||
// Limit set size to prevent memory issues (keep last 500)
|
||||
if (seenMessageContent.current.size > 1000) {
|
||||
const entries = Array.from(seenMessageContent.current);
|
||||
seenMessageContent.current = new Set(entries.slice(-500));
|
||||
}
|
||||
|
||||
setMessages((prev) => {
|
||||
if (prev.some((m) => m.id === msg.id)) {
|
||||
return prev;
|
||||
}
|
||||
return [...prev, msg];
|
||||
});
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
// Update a message's ack count
|
||||
const updateMessageAck = useCallback((messageId: number, ackCount: number) => {
|
||||
setMessages((prev) => {
|
||||
const idx = prev.findIndex((m) => m.id === messageId);
|
||||
if (idx >= 0) {
|
||||
const updated = [...prev];
|
||||
updated[idx] = { ...prev[idx], acked: ackCount };
|
||||
return updated;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
messages,
|
||||
messagesLoading,
|
||||
loadingOlder,
|
||||
hasOlderMessages,
|
||||
setMessages,
|
||||
fetchMessages,
|
||||
fetchOlderMessages,
|
||||
addMessageIfNew,
|
||||
updateMessageAck,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { api } from '../api';
|
||||
import type { Contact, Conversation, Message, TelemetryResponse, NeighborInfo, AclEntry } from '../types';
|
||||
import { CONTACT_TYPE_REPEATER } from '../types';
|
||||
|
||||
// Format seconds into human-readable duration (e.g., 1d17h2m, 1h5m, 3m)
|
||||
export function formatDuration(seconds: number): string {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
if (hours > 0 && mins > 0) return `${days}d${hours}h${mins}m`;
|
||||
if (hours > 0) return `${days}d${hours}h`;
|
||||
if (mins > 0) return `${days}d${mins}m`;
|
||||
return `${days}d`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
|
||||
}
|
||||
return `${mins}m`;
|
||||
}
|
||||
|
||||
// Format telemetry response as human-readable text
|
||||
export function formatTelemetry(telemetry: TelemetryResponse): string {
|
||||
const lines = [
|
||||
`Telemetry`,
|
||||
`Battery Voltage: ${telemetry.battery_volts.toFixed(3)}V`,
|
||||
`Uptime: ${formatDuration(telemetry.uptime_seconds)}`,
|
||||
`TX Airtime: ${formatDuration(telemetry.airtime_seconds)}`,
|
||||
`RX Airtime: ${formatDuration(telemetry.rx_airtime_seconds)}`,
|
||||
'',
|
||||
`Noise Floor: ${telemetry.noise_floor_dbm} dBm`,
|
||||
`Last RSSI: ${telemetry.last_rssi_dbm} dBm`,
|
||||
`Last SNR: ${telemetry.last_snr_db.toFixed(1)} dB`,
|
||||
'',
|
||||
`Packets: ${telemetry.packets_received.toLocaleString()} rx / ${telemetry.packets_sent.toLocaleString()} tx`,
|
||||
`Flood: ${telemetry.recv_flood.toLocaleString()} rx / ${telemetry.sent_flood.toLocaleString()} tx`,
|
||||
`Direct: ${telemetry.recv_direct.toLocaleString()} rx / ${telemetry.sent_direct.toLocaleString()} tx`,
|
||||
`Duplicates: ${telemetry.flood_dups.toLocaleString()} flood / ${telemetry.direct_dups.toLocaleString()} direct`,
|
||||
'',
|
||||
`TX Queue: ${telemetry.tx_queue_len}`,
|
||||
`Debug Flags: ${telemetry.full_events}`,
|
||||
];
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Format neighbors list as human-readable text
|
||||
export function formatNeighbors(neighbors: NeighborInfo[]): string {
|
||||
if (neighbors.length === 0) {
|
||||
return 'Neighbors\nNo neighbors reported';
|
||||
}
|
||||
// Sort by SNR descending (highest first)
|
||||
const sorted = [...neighbors].sort((a, b) => b.snr - a.snr);
|
||||
const lines = [`Neighbors (${sorted.length})`];
|
||||
for (const n of sorted) {
|
||||
const name = n.name || n.pubkey_prefix;
|
||||
const snr = n.snr >= 0 ? `+${n.snr.toFixed(1)}` : n.snr.toFixed(1);
|
||||
lines.push(`${name}, ${snr} dB [${formatDuration(n.last_heard_seconds)} ago]`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Format ACL list as human-readable text
|
||||
export function formatAcl(acl: AclEntry[]): string {
|
||||
if (acl.length === 0) {
|
||||
return 'ACL\nNo ACL entries';
|
||||
}
|
||||
const lines = [`ACL (${acl.length})`];
|
||||
for (const entry of acl) {
|
||||
const name = entry.name || entry.pubkey_prefix;
|
||||
lines.push(`${name}: ${entry.permission_name}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Create a local message object (not persisted to database)
|
||||
function createLocalMessage(
|
||||
conversationKey: string,
|
||||
text: string,
|
||||
outgoing: boolean,
|
||||
idOffset = 0
|
||||
): Message {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return {
|
||||
id: -Date.now() - idOffset,
|
||||
type: 'PRIV',
|
||||
conversation_key: conversationKey,
|
||||
text,
|
||||
sender_timestamp: now,
|
||||
received_at: now,
|
||||
path_len: null,
|
||||
txt_type: 0,
|
||||
signature: null,
|
||||
outgoing,
|
||||
acked: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseRepeaterModeResult {
|
||||
repeaterLoggedIn: boolean;
|
||||
activeContactIsRepeater: boolean;
|
||||
handleTelemetryRequest: (password: string) => Promise<void>;
|
||||
handleRepeaterCommand: (command: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useRepeaterMode(
|
||||
activeConversation: Conversation | null,
|
||||
contacts: Contact[],
|
||||
setMessages: React.Dispatch<React.SetStateAction<Message[]>>
|
||||
): UseRepeaterModeResult {
|
||||
const [repeaterLoggedIn, setRepeaterLoggedIn] = useState(false);
|
||||
|
||||
// Reset login state when conversation changes
|
||||
useEffect(() => {
|
||||
setRepeaterLoggedIn(false);
|
||||
}, [activeConversation?.id]);
|
||||
|
||||
// Check if active conversation is a repeater
|
||||
const activeContactIsRepeater = useMemo(() => {
|
||||
if (!activeConversation || activeConversation.type !== 'contact') return false;
|
||||
const contact = contacts.find(c => c.public_key === activeConversation.id);
|
||||
return contact?.type === CONTACT_TYPE_REPEATER;
|
||||
}, [activeConversation, contacts]);
|
||||
|
||||
// Request telemetry from a repeater
|
||||
const handleTelemetryRequest = useCallback(
|
||||
async (password: string) => {
|
||||
if (!activeConversation || activeConversation.type !== 'contact') return;
|
||||
if (!activeContactIsRepeater) return;
|
||||
|
||||
try {
|
||||
const telemetry = await api.requestTelemetry(activeConversation.id, password);
|
||||
|
||||
// Create local messages to display the telemetry (not persisted to database)
|
||||
const telemetryMessage = createLocalMessage(
|
||||
activeConversation.id,
|
||||
formatTelemetry(telemetry),
|
||||
false,
|
||||
0
|
||||
);
|
||||
|
||||
const neighborsMessage = createLocalMessage(
|
||||
activeConversation.id,
|
||||
formatNeighbors(telemetry.neighbors),
|
||||
false,
|
||||
1
|
||||
);
|
||||
|
||||
const aclMessage = createLocalMessage(
|
||||
activeConversation.id,
|
||||
formatAcl(telemetry.acl),
|
||||
false,
|
||||
2
|
||||
);
|
||||
|
||||
// Add all messages to the list
|
||||
setMessages((prev) => [...prev, telemetryMessage, neighborsMessage, aclMessage]);
|
||||
|
||||
// Mark as logged in for CLI command mode
|
||||
setRepeaterLoggedIn(true);
|
||||
} catch (err) {
|
||||
const errorMessage = createLocalMessage(
|
||||
activeConversation.id,
|
||||
`Telemetry request failed: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
||||
false,
|
||||
0
|
||||
);
|
||||
setMessages((prev) => [...prev, errorMessage]);
|
||||
}
|
||||
},
|
||||
[activeConversation, activeContactIsRepeater, setMessages]
|
||||
);
|
||||
|
||||
// Send CLI command to a repeater (after logged in)
|
||||
const handleRepeaterCommand = useCallback(
|
||||
async (command: string) => {
|
||||
if (!activeConversation || activeConversation.type !== 'contact') return;
|
||||
if (!activeContactIsRepeater || !repeaterLoggedIn) return;
|
||||
|
||||
// Show the command as an outgoing message
|
||||
const commandMessage = createLocalMessage(
|
||||
activeConversation.id,
|
||||
`> ${command}`,
|
||||
true,
|
||||
0
|
||||
);
|
||||
setMessages((prev) => [...prev, commandMessage]);
|
||||
|
||||
try {
|
||||
const response = await api.sendRepeaterCommand(activeConversation.id, command);
|
||||
|
||||
// Use the actual timestamp from the repeater if available
|
||||
const responseMessage = createLocalMessage(
|
||||
activeConversation.id,
|
||||
response.response,
|
||||
false,
|
||||
1
|
||||
);
|
||||
if (response.sender_timestamp) {
|
||||
responseMessage.sender_timestamp = response.sender_timestamp;
|
||||
}
|
||||
|
||||
setMessages((prev) => [...prev, responseMessage]);
|
||||
} catch (err) {
|
||||
const errorMessage = createLocalMessage(
|
||||
activeConversation.id,
|
||||
`Command failed: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
||||
false,
|
||||
1
|
||||
);
|
||||
setMessages((prev) => [...prev, errorMessage]);
|
||||
}
|
||||
},
|
||||
[activeConversation, activeContactIsRepeater, repeaterLoggedIn, setMessages]
|
||||
);
|
||||
|
||||
return {
|
||||
repeaterLoggedIn,
|
||||
activeContactIsRepeater,
|
||||
handleTelemetryRequest,
|
||||
handleRepeaterCommand,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { api } from '../api';
|
||||
import {
|
||||
getLastMessageTimes,
|
||||
getLastReadTimes,
|
||||
setLastMessageTime,
|
||||
setLastReadTime,
|
||||
getStateKey,
|
||||
type ConversationTimes,
|
||||
} from '../utils/conversationState';
|
||||
import type { Channel, Contact, Conversation, Message } from '../types';
|
||||
|
||||
export interface UseUnreadCountsResult {
|
||||
unreadCounts: Record<string, number>;
|
||||
lastMessageTimes: ConversationTimes;
|
||||
incrementUnread: (stateKey: string) => void;
|
||||
markAllRead: () => void;
|
||||
markConversationRead: (conv: Conversation) => void;
|
||||
trackNewMessage: (msg: Message) => void;
|
||||
}
|
||||
|
||||
export function useUnreadCounts(
|
||||
channels: Channel[],
|
||||
contacts: Contact[],
|
||||
activeConversation: Conversation | null
|
||||
): UseUnreadCountsResult {
|
||||
const [unreadCounts, setUnreadCounts] = useState<Record<string, number>>({});
|
||||
const [lastMessageTimes, setLastMessageTimes] = useState<ConversationTimes>(getLastMessageTimes);
|
||||
|
||||
// Track which channels/contacts we've already fetched unreads for
|
||||
const fetchedChannels = useRef<Set<string>>(new Set());
|
||||
const fetchedContacts = useRef<Set<string>>(new Set());
|
||||
|
||||
// Fetch messages and count unreads for new channels/contacts
|
||||
useEffect(() => {
|
||||
const newChannels = channels.filter(c => !fetchedChannels.current.has(c.key));
|
||||
const newContacts = contacts.filter(c => c.public_key && !fetchedContacts.current.has(c.public_key));
|
||||
|
||||
if (newChannels.length === 0 && newContacts.length === 0) return;
|
||||
|
||||
// Mark as fetched before starting (to avoid duplicate fetches if effect re-runs)
|
||||
newChannels.forEach(c => fetchedChannels.current.add(c.key));
|
||||
newContacts.forEach(c => fetchedContacts.current.add(c.public_key));
|
||||
|
||||
const fetchAndCountUnreads = async () => {
|
||||
const conversations: Array<{ type: 'PRIV' | 'CHAN'; conversation_key: string }> = [
|
||||
...newChannels.map(c => ({ type: 'CHAN' as const, conversation_key: c.key })),
|
||||
...newContacts.map(c => ({ type: 'PRIV' as const, conversation_key: c.public_key })),
|
||||
];
|
||||
|
||||
if (conversations.length === 0) return;
|
||||
|
||||
try {
|
||||
const bulkMessages = await api.getMessagesBulk(conversations, 100);
|
||||
const currentReadTimes = getLastReadTimes();
|
||||
const newUnreadCounts: Record<string, number> = {};
|
||||
const newLastMessageTimes: Record<string, number> = {};
|
||||
|
||||
// Process channel messages
|
||||
for (const channel of newChannels) {
|
||||
const msgs = bulkMessages[`CHAN:${channel.key}`] || [];
|
||||
if (msgs.length > 0) {
|
||||
const key = getStateKey('channel', channel.key);
|
||||
const lastRead = currentReadTimes[key] || 0;
|
||||
|
||||
const unreadCount = msgs.filter(m => !m.outgoing && m.received_at > lastRead).length;
|
||||
if (unreadCount > 0) {
|
||||
newUnreadCounts[key] = unreadCount;
|
||||
}
|
||||
|
||||
const latestTime = Math.max(...msgs.map(m => m.received_at));
|
||||
newLastMessageTimes[key] = latestTime;
|
||||
setLastMessageTime(key, latestTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Process contact messages
|
||||
for (const contact of newContacts) {
|
||||
const msgs = bulkMessages[`PRIV:${contact.public_key}`] || [];
|
||||
if (msgs.length > 0) {
|
||||
const key = getStateKey('contact', contact.public_key);
|
||||
const lastRead = currentReadTimes[key] || 0;
|
||||
|
||||
const unreadCount = msgs.filter(m => !m.outgoing && m.received_at > lastRead).length;
|
||||
if (unreadCount > 0) {
|
||||
newUnreadCounts[key] = unreadCount;
|
||||
}
|
||||
|
||||
const latestTime = Math.max(...msgs.map(m => m.received_at));
|
||||
newLastMessageTimes[key] = latestTime;
|
||||
setLastMessageTime(key, latestTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(newUnreadCounts).length > 0) {
|
||||
setUnreadCounts(prev => ({ ...prev, ...newUnreadCounts }));
|
||||
}
|
||||
setLastMessageTimes(getLastMessageTimes());
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch messages bulk:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAndCountUnreads();
|
||||
}, [channels, contacts]);
|
||||
|
||||
// Mark conversation as read when user views it
|
||||
useEffect(() => {
|
||||
if (activeConversation && activeConversation.type !== 'raw') {
|
||||
const key = getStateKey(
|
||||
activeConversation.type as 'channel' | 'contact',
|
||||
activeConversation.id
|
||||
);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
setLastReadTime(key, now);
|
||||
|
||||
setUnreadCounts((prev) => {
|
||||
if (prev[key]) {
|
||||
const next = { ...prev };
|
||||
delete next[key];
|
||||
return next;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
}, [activeConversation]);
|
||||
|
||||
// Increment unread count for a conversation
|
||||
const incrementUnread = useCallback((stateKey: string) => {
|
||||
setUnreadCounts((prev) => ({
|
||||
...prev,
|
||||
[stateKey]: (prev[stateKey] || 0) + 1,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Mark all conversations as read
|
||||
const markAllRead = useCallback(() => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
for (const channel of channels) {
|
||||
const key = getStateKey('channel', channel.key);
|
||||
setLastReadTime(key, now);
|
||||
}
|
||||
|
||||
for (const contact of contacts) {
|
||||
if (contact.public_key) {
|
||||
const key = getStateKey('contact', contact.public_key);
|
||||
setLastReadTime(key, now);
|
||||
}
|
||||
}
|
||||
|
||||
setUnreadCounts({});
|
||||
}, [channels, contacts]);
|
||||
|
||||
// Mark a specific conversation as read
|
||||
const markConversationRead = useCallback((conv: Conversation) => {
|
||||
if (conv.type === 'raw') return;
|
||||
|
||||
const key = getStateKey(conv.type as 'channel' | 'contact', conv.id);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
setLastReadTime(key, now);
|
||||
|
||||
setUnreadCounts((prev) => {
|
||||
if (prev[key]) {
|
||||
const next = { ...prev };
|
||||
delete next[key];
|
||||
return next;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Track a new incoming message for unread counts
|
||||
const trackNewMessage = useCallback((msg: Message) => {
|
||||
let conversationKey: string | null = null;
|
||||
if (msg.type === 'CHAN' && msg.conversation_key) {
|
||||
conversationKey = getStateKey('channel', msg.conversation_key);
|
||||
} else if (msg.type === 'PRIV' && msg.conversation_key) {
|
||||
conversationKey = getStateKey('contact', msg.conversation_key);
|
||||
}
|
||||
|
||||
if (conversationKey) {
|
||||
const timestamp = msg.received_at || Math.floor(Date.now() / 1000);
|
||||
const updated = setLastMessageTime(conversationKey, timestamp);
|
||||
setLastMessageTimes(updated);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
unreadCounts,
|
||||
lastMessageTimes,
|
||||
incrementUnread,
|
||||
markAllRead,
|
||||
markConversationRead,
|
||||
trackNewMessage,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user