mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-06 16:53:38 +02:00
Add resend button for 30s
This commit is contained in:
@@ -666,6 +666,18 @@ export function App() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle resend channel message
|
||||
const handleResendChannelMessage = useCallback(async (messageId: number) => {
|
||||
try {
|
||||
await api.resendChannelMessage(messageId);
|
||||
toast.success('Message resent');
|
||||
} catch (err) {
|
||||
toast.error('Failed to resend', {
|
||||
description: err instanceof Error ? err.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle sender click to add mention
|
||||
const handleSenderClick = useCallback((sender: string) => {
|
||||
messageInputRef.current?.appendText(`@[${sender}] `);
|
||||
@@ -1182,6 +1194,9 @@ export function App() {
|
||||
activeConversation.type === 'channel' ? handleSenderClick : undefined
|
||||
}
|
||||
onLoadOlder={fetchOlderMessages}
|
||||
onResendChannelMessage={
|
||||
activeConversation.type === 'channel' ? handleResendChannelMessage : undefined
|
||||
}
|
||||
radioName={config?.name}
|
||||
config={config}
|
||||
/>
|
||||
|
||||
@@ -166,6 +166,10 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ channel_key: channelKey, text }),
|
||||
}),
|
||||
resendChannelMessage: (messageId: number) =>
|
||||
fetchJson<{ status: string; message_id: number }>(`/messages/channel/${messageId}/resend`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// Packets
|
||||
getUndecryptedPacketCount: () => fetchJson<{ count: number }>('/packets/undecrypted/count'),
|
||||
|
||||
@@ -23,6 +23,7 @@ interface MessageListProps {
|
||||
hasOlderMessages?: boolean;
|
||||
onSenderClick?: (sender: string) => void;
|
||||
onLoadOlder?: () => void;
|
||||
onResendChannelMessage?: (messageId: number) => void;
|
||||
radioName?: string;
|
||||
config?: RadioConfig | null;
|
||||
}
|
||||
@@ -134,6 +135,8 @@ function HopCountBadge({ paths, onClick, variant }: HopCountBadgeProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const RESEND_WINDOW_SECONDS = 30;
|
||||
|
||||
export function MessageList({
|
||||
messages,
|
||||
contacts,
|
||||
@@ -142,6 +145,7 @@ export function MessageList({
|
||||
hasOlderMessages = false,
|
||||
onSenderClick,
|
||||
onLoadOlder,
|
||||
onResendChannelMessage,
|
||||
radioName,
|
||||
config,
|
||||
}: MessageListProps) {
|
||||
@@ -153,6 +157,8 @@ export function MessageList({
|
||||
paths: MessagePath[];
|
||||
senderInfo: SenderInfo;
|
||||
} | null>(null);
|
||||
const [resendableIds, setResendableIds] = useState<Set<number>>(new Set());
|
||||
const resendTimersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());
|
||||
|
||||
// Capture scroll state in the scroll handler BEFORE any state updates
|
||||
const scrollStateRef = useRef({
|
||||
@@ -216,6 +222,43 @@ export function MessageList({
|
||||
}
|
||||
}, [messages.length]);
|
||||
|
||||
// Track resendable outgoing CHAN messages (within 30s window)
|
||||
useEffect(() => {
|
||||
if (!onResendChannelMessage) return;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const newResendable = new Set<number>();
|
||||
const timers = resendTimersRef.current;
|
||||
|
||||
for (const msg of messages) {
|
||||
if (!msg.outgoing || msg.type !== 'CHAN' || msg.sender_timestamp === null) continue;
|
||||
const remaining = RESEND_WINDOW_SECONDS - (now - msg.sender_timestamp);
|
||||
if (remaining <= 0) continue;
|
||||
|
||||
newResendable.add(msg.id);
|
||||
|
||||
// Schedule removal if not already tracked
|
||||
if (!timers.has(msg.id)) {
|
||||
const timer = setTimeout(() => {
|
||||
setResendableIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(msg.id);
|
||||
return next;
|
||||
});
|
||||
timers.delete(msg.id);
|
||||
}, remaining * 1000);
|
||||
timers.set(msg.id, timer);
|
||||
}
|
||||
}
|
||||
|
||||
setResendableIds(newResendable);
|
||||
|
||||
return () => {
|
||||
for (const timer of timers.values()) clearTimeout(timer);
|
||||
timers.clear();
|
||||
};
|
||||
}, [messages, onResendChannelMessage]);
|
||||
|
||||
// Handle scroll - capture state and detect when user is near top/bottom
|
||||
const handleScroll = useCallback(() => {
|
||||
if (!listRef.current) return;
|
||||
@@ -463,11 +506,23 @@ export function MessageList({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{msg.outgoing && onResendChannelMessage && resendableIds.has(msg.id) && (
|
||||
<button
|
||||
className="text-muted-foreground hover:text-primary ml-1 text-xs cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResendChannelMessage(msg.id);
|
||||
}}
|
||||
title="Resend message"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
)}
|
||||
{msg.outgoing &&
|
||||
(msg.acked > 0 ? (
|
||||
msg.paths && msg.paths.length > 0 ? (
|
||||
<span
|
||||
className="cursor-pointer hover:text-primary"
|
||||
className="text-muted-foreground cursor-pointer hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedPath({
|
||||
@@ -483,10 +538,13 @@ export function MessageList({
|
||||
title="View echo paths"
|
||||
>{` ✓${msg.acked > 1 ? msg.acked : ''}`}</span>
|
||||
) : (
|
||||
` ✓${msg.acked > 1 ? msg.acked : ''}`
|
||||
<span className="text-muted-foreground">{` ✓${msg.acked > 1 ? msg.acked : ''}`}</span>
|
||||
)
|
||||
) : (
|
||||
<span title="No repeats heard yet"> ?</span>
|
||||
<span className="text-muted-foreground" title="No repeats heard yet">
|
||||
{' '}
|
||||
?
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -123,7 +123,6 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
const [cr, setCr] = useState('');
|
||||
const [privateKey, setPrivateKey] = useState('');
|
||||
const [maxRadioContacts, setMaxRadioContacts] = useState('');
|
||||
const [experimentalChannelDoubleSend, setExperimentalChannelDoubleSend] = useState(false);
|
||||
|
||||
// Loading states
|
||||
const [busySection, setBusySection] = useState<SettingsSection | null>(null);
|
||||
@@ -202,7 +201,6 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
useEffect(() => {
|
||||
if (appSettings) {
|
||||
setMaxRadioContacts(String(appSettings.max_radio_contacts));
|
||||
setExperimentalChannelDoubleSend(appSettings.experimental_channel_double_send);
|
||||
setAutoDecryptOnAdvert(appSettings.auto_decrypt_dm_on_advert);
|
||||
setAdvertInterval(String(appSettings.advert_interval));
|
||||
setBots(appSettings.bots || []);
|
||||
@@ -368,9 +366,6 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
if (!isNaN(newMaxRadioContacts) && newMaxRadioContacts !== appSettings?.max_radio_contacts) {
|
||||
update.max_radio_contacts = newMaxRadioContacts;
|
||||
}
|
||||
if (experimentalChannelDoubleSend !== appSettings?.experimental_channel_double_send) {
|
||||
update.experimental_channel_double_send = experimentalChannelDoubleSend;
|
||||
}
|
||||
if (Object.keys(update).length > 0) {
|
||||
await onSaveAppSettings(update);
|
||||
}
|
||||
@@ -900,27 +895,6 @@ export function SettingsModal(props: SettingsModalProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-md space-y-3">
|
||||
<p className="text-sm text-yellow-500">
|
||||
<strong>Experimental:</strong> Adds a duplicate channel send after a 3-second
|
||||
delay, using the exact same timestamp bytes.
|
||||
</p>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={experimentalChannelDoubleSend}
|
||||
onChange={(e) => setExperimentalChannelDoubleSend(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-input accent-primary"
|
||||
/>
|
||||
<span className="text-sm">Always send channel messages twice</span>
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This increases channel airtime and adds a 3-second second-attempt delay. Most
|
||||
clients deduplicate repeats by payload and timestamp, but behavior can vary by
|
||||
firmware/client.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleSaveConnectivity}
|
||||
disabled={isSectionBusy('connectivity')}
|
||||
|
||||
@@ -182,7 +182,6 @@ const baseConfig = {
|
||||
|
||||
const baseSettings = {
|
||||
max_radio_contacts: 200,
|
||||
experimental_channel_double_send: false,
|
||||
favorites: [] as Array<{ type: 'channel' | 'contact'; id: string }>,
|
||||
auto_decrypt_dm_on_advert: false,
|
||||
sidebar_sort_order: 'recent' as const,
|
||||
|
||||
@@ -158,7 +158,6 @@ describe('App startup hash resolution', () => {
|
||||
});
|
||||
mocks.api.getSettings.mockResolvedValue({
|
||||
max_radio_contacts: 200,
|
||||
experimental_channel_double_send: false,
|
||||
favorites: [],
|
||||
auto_decrypt_dm_on_advert: false,
|
||||
sidebar_sort_order: 'recent',
|
||||
|
||||
@@ -36,7 +36,6 @@ const baseHealth: HealthStatus = {
|
||||
|
||||
const baseSettings: AppSettings = {
|
||||
max_radio_contacts: 200,
|
||||
experimental_channel_double_send: false,
|
||||
favorites: [],
|
||||
auto_decrypt_dm_on_advert: false,
|
||||
sidebar_sort_order: 'recent',
|
||||
@@ -194,24 +193,6 @@ describe('SettingsModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('saves experimental channel double-send toggle through onSaveAppSettings', async () => {
|
||||
const { onSaveAppSettings } = renderModal({
|
||||
appSettings: { ...baseSettings, experimental_channel_double_send: false },
|
||||
});
|
||||
|
||||
openConnectivitySection();
|
||||
|
||||
const toggle = screen.getByLabelText('Always send channel messages twice');
|
||||
fireEvent.click(toggle);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save Settings' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSaveAppSettings).toHaveBeenCalledWith({
|
||||
experimental_channel_double_send: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders selected section from external sidebar nav on desktop mode', async () => {
|
||||
renderModal({
|
||||
externalSidebarNav: true,
|
||||
|
||||
@@ -124,7 +124,6 @@ export interface BotConfig {
|
||||
|
||||
export interface AppSettings {
|
||||
max_radio_contacts: number;
|
||||
experimental_channel_double_send: boolean;
|
||||
favorites: Favorite[];
|
||||
auto_decrypt_dm_on_advert: boolean;
|
||||
sidebar_sort_order: 'recent' | 'alpha';
|
||||
@@ -137,7 +136,6 @@ export interface AppSettings {
|
||||
|
||||
export interface AppSettingsUpdate {
|
||||
max_radio_contacts?: number;
|
||||
experimental_channel_double_send?: boolean;
|
||||
auto_decrypt_dm_on_advert?: boolean;
|
||||
sidebar_sort_order?: 'recent' | 'alpha';
|
||||
advert_interval?: number;
|
||||
|
||||
Reference in New Issue
Block a user