mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 17:23:05 +02:00
Add bell icon and use better notif icon
This commit is contained in:
@@ -270,6 +270,7 @@ export function App() {
|
|||||||
onSortOrderChange: (sortOrder: 'recent' | 'alpha') => {
|
onSortOrderChange: (sortOrder: 'recent' | 'alpha') => {
|
||||||
void handleSortOrderChange(sortOrder);
|
void handleSortOrderChange(sortOrder);
|
||||||
},
|
},
|
||||||
|
isConversationNotificationsEnabled,
|
||||||
};
|
};
|
||||||
const conversationPaneProps = {
|
const conversationPaneProps = {
|
||||||
activeConversation,
|
activeConversation,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
Bell,
|
||||||
CheckCheck,
|
CheckCheck,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -36,6 +37,7 @@ type ConversationRow = {
|
|||||||
name: string;
|
name: string;
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
isMention: boolean;
|
isMention: boolean;
|
||||||
|
notificationsEnabled: boolean;
|
||||||
contact?: Contact;
|
contact?: Contact;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,6 +95,7 @@ interface SidebarProps {
|
|||||||
sortOrder?: SortOrder;
|
sortOrder?: SortOrder;
|
||||||
/** Callback when sort order changes */
|
/** Callback when sort order changes */
|
||||||
onSortOrderChange?: (order: SortOrder) => void;
|
onSortOrderChange?: (order: SortOrder) => void;
|
||||||
|
isConversationNotificationsEnabled?: (type: 'channel' | 'contact', id: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
@@ -111,6 +114,7 @@ export function Sidebar({
|
|||||||
favorites,
|
favorites,
|
||||||
sortOrder: sortOrderProp = 'recent',
|
sortOrder: sortOrderProp = 'recent',
|
||||||
onSortOrderChange,
|
onSortOrderChange,
|
||||||
|
isConversationNotificationsEnabled,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const sortOrder = sortOrderProp;
|
const sortOrder = sortOrderProp;
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -405,6 +409,7 @@ export function Sidebar({
|
|||||||
name: channel.name,
|
name: channel.name,
|
||||||
unreadCount: getUnreadCount('channel', channel.key),
|
unreadCount: getUnreadCount('channel', channel.key),
|
||||||
isMention: hasMention('channel', channel.key),
|
isMention: hasMention('channel', channel.key),
|
||||||
|
notificationsEnabled: isConversationNotificationsEnabled?.('channel', channel.key) ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildContactRow = (contact: Contact, keyPrefix: string): ConversationRow => ({
|
const buildContactRow = (contact: Contact, keyPrefix: string): ConversationRow => ({
|
||||||
@@ -414,6 +419,8 @@ export function Sidebar({
|
|||||||
name: getContactDisplayName(contact.name, contact.public_key),
|
name: getContactDisplayName(contact.name, contact.public_key),
|
||||||
unreadCount: getUnreadCount('contact', contact.public_key),
|
unreadCount: getUnreadCount('contact', contact.public_key),
|
||||||
isMention: hasMention('contact', contact.public_key),
|
isMention: hasMention('contact', contact.public_key),
|
||||||
|
notificationsEnabled:
|
||||||
|
isConversationNotificationsEnabled?.('contact', contact.public_key) ?? false,
|
||||||
contact,
|
contact,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -446,19 +453,26 @@ export function Sidebar({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="name flex-1 truncate text-[13px]">{row.name}</span>
|
<span className="name flex-1 truncate text-[13px]">{row.name}</span>
|
||||||
{row.unreadCount > 0 && (
|
<span className="ml-auto flex items-center gap-1">
|
||||||
<span
|
{row.notificationsEnabled && (
|
||||||
className={cn(
|
<span aria-label="Notifications enabled" title="Notifications enabled">
|
||||||
'text-[10px] font-semibold px-1.5 py-0.5 rounded-full min-w-[18px] text-center',
|
<Bell className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
row.isMention
|
</span>
|
||||||
? 'bg-badge-mention text-badge-mention-foreground'
|
)}
|
||||||
: 'bg-badge-unread/90 text-badge-unread-foreground'
|
{row.unreadCount > 0 && (
|
||||||
)}
|
<span
|
||||||
aria-label={`${row.unreadCount} unread message${row.unreadCount !== 1 ? 's' : ''}`}
|
className={cn(
|
||||||
>
|
'text-[10px] font-semibold px-1.5 py-0.5 rounded-full min-w-[18px] text-center',
|
||||||
{row.unreadCount}
|
row.isMention
|
||||||
</span>
|
? 'bg-badge-mention text-badge-mention-foreground'
|
||||||
)}
|
: 'bg-badge-unread/90 text-badge-unread-foreground'
|
||||||
|
)}
|
||||||
|
aria-label={`${row.unreadCount} unread message${row.unreadCount !== 1 ? 's' : ''}`}
|
||||||
|
>
|
||||||
|
{row.unreadCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Message } from '../types';
|
|||||||
import { getStateKey } from '../utils/conversationState';
|
import { getStateKey } from '../utils/conversationState';
|
||||||
|
|
||||||
const STORAGE_KEY = 'meshcore_browser_notifications_enabled_by_conversation';
|
const STORAGE_KEY = 'meshcore_browser_notifications_enabled_by_conversation';
|
||||||
const NOTIFICATION_ICON_PATH = '/apple-touch-icon.png';
|
const NOTIFICATION_ICON_PATH = '/favicon-256x256.png';
|
||||||
|
|
||||||
type NotificationPermissionState = NotificationPermission | 'unsupported';
|
type NotificationPermissionState = NotificationPermission | 'unsupported';
|
||||||
type ConversationNotificationMap = Record<string, boolean>;
|
type ConversationNotificationMap = Record<string, boolean>;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ function renderSidebar(overrides?: {
|
|||||||
favorites?: Favorite[];
|
favorites?: Favorite[];
|
||||||
lastMessageTimes?: ConversationTimes;
|
lastMessageTimes?: ConversationTimes;
|
||||||
channels?: Channel[];
|
channels?: Channel[];
|
||||||
|
isConversationNotificationsEnabled?: (type: 'channel' | 'contact', id: string) => boolean;
|
||||||
}) {
|
}) {
|
||||||
const aliceName = 'Alice';
|
const aliceName = 'Alice';
|
||||||
const publicChannel = makeChannel('AA'.repeat(16), 'Public');
|
const publicChannel = makeChannel('AA'.repeat(16), 'Public');
|
||||||
@@ -76,6 +77,7 @@ function renderSidebar(overrides?: {
|
|||||||
favorites={favorites}
|
favorites={favorites}
|
||||||
sortOrder="recent"
|
sortOrder="recent"
|
||||||
onSortOrderChange={vi.fn()}
|
onSortOrderChange={vi.fn()}
|
||||||
|
isConversationNotificationsEnabled={overrides?.isConversationNotificationsEnabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -218,4 +220,37 @@ describe('Sidebar section summaries', () => {
|
|||||||
const selectedIds = onSelectConversation.mock.calls.map(([conv]) => conv.id);
|
const selectedIds = onSelectConversation.mock.calls.map(([conv]) => conv.id);
|
||||||
expect(new Set(selectedIds)).toEqual(new Set([channelA.key, channelB.key]));
|
expect(new Set(selectedIds)).toEqual(new Set([channelA.key, channelB.key]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows a notification bell for conversations with notifications enabled', () => {
|
||||||
|
const { aliceName } = renderSidebar({
|
||||||
|
unreadCounts: {},
|
||||||
|
isConversationNotificationsEnabled: (type, id) =>
|
||||||
|
(type === 'contact' && id === '11'.repeat(32)) ||
|
||||||
|
(type === 'channel' && id === 'BB'.repeat(16)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const aliceRow = screen.getByText(aliceName).closest('div');
|
||||||
|
const flightRow = screen.getByText('#flight').closest('div');
|
||||||
|
if (!aliceRow || !flightRow) throw new Error('Missing sidebar rows');
|
||||||
|
|
||||||
|
expect(within(aliceRow).getByLabelText('Notifications enabled')).toBeInTheDocument();
|
||||||
|
expect(within(flightRow).getByLabelText('Notifications enabled')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the notification bell to the left of the unread pill when both are present', () => {
|
||||||
|
const { aliceName } = renderSidebar({
|
||||||
|
unreadCounts: {
|
||||||
|
[getStateKey('contact', '11'.repeat(32))]: 3,
|
||||||
|
},
|
||||||
|
isConversationNotificationsEnabled: (type, id) =>
|
||||||
|
type === 'contact' && id === '11'.repeat(32),
|
||||||
|
});
|
||||||
|
|
||||||
|
const aliceRow = screen.getByText(aliceName).closest('div');
|
||||||
|
if (!aliceRow) throw new Error('Missing Alice row');
|
||||||
|
|
||||||
|
const bell = within(aliceRow).getByLabelText('Notifications enabled');
|
||||||
|
const unread = within(aliceRow).getByText('3');
|
||||||
|
expect(bell.compareDocumentPosition(unread) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ describe('useBrowserNotifications', () => {
|
|||||||
);
|
);
|
||||||
expect(window.Notification).toHaveBeenCalledWith('New message in #flightless', {
|
expect(window.Notification).toHaveBeenCalledWith('New message in #flightless', {
|
||||||
body: 'Notifications will look like this. These require the tab to stay open, and will not be reliable on mobile.',
|
body: 'Notifications will look like this. These require the tab to stay open, and will not be reliable on mobile.',
|
||||||
icon: '/apple-touch-icon.png',
|
icon: '/favicon-256x256.png',
|
||||||
tag: `meshcore-notification-preview-channel-${incomingChannelMessage.conversation_key}`,
|
tag: `meshcore-notification-preview-channel-${incomingChannelMessage.conversation_key}`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -110,7 +110,7 @@ describe('useBrowserNotifications', () => {
|
|||||||
expect(window.Notification).toHaveBeenCalledTimes(2);
|
expect(window.Notification).toHaveBeenCalledTimes(2);
|
||||||
expect(window.Notification).toHaveBeenNthCalledWith(2, 'New message in #flightless', {
|
expect(window.Notification).toHaveBeenNthCalledWith(2, 'New message in #flightless', {
|
||||||
body: 'hello room',
|
body: 'hello room',
|
||||||
icon: '/apple-touch-icon.png',
|
icon: '/favicon-256x256.png',
|
||||||
tag: 'meshcore-message-42',
|
tag: 'meshcore-message-42',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user