mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Display account read state indicator
This commit is contained in:
@@ -4,14 +4,13 @@ import { WorkspaceDatabaseSchema } from '@/main/data/workspace/schema';
|
||||
import { getIdType, IdType, NodeTypes } from '@colanode/core';
|
||||
import { WorkspaceReadState } from '@/shared/types/radars';
|
||||
import { eventBus } from '@/shared/lib/event-bus';
|
||||
import { Event } from '@/shared/types/events';
|
||||
|
||||
class RadarService {
|
||||
private readonly workspaceStates: Record<string, WorkspaceReadState> = {};
|
||||
|
||||
constructor() {
|
||||
eventBus.subscribe((event) => {
|
||||
console.log('event', event);
|
||||
});
|
||||
eventBus.subscribe(this.handleEvent.bind(this));
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
@@ -88,6 +87,18 @@ class RadarService {
|
||||
|
||||
this.workspaceStates[userId] = workspaceState;
|
||||
}
|
||||
|
||||
private async handleEvent(event: Event) {
|
||||
if (event.type === 'workspace_deleted') {
|
||||
delete this.workspaceStates[event.workspace.userId];
|
||||
} else if (event.type === 'user_node_created') {
|
||||
// to be optimized
|
||||
const workspaceDatabase = await databaseService.getWorkspaceDatabase(
|
||||
event.userId
|
||||
);
|
||||
await this.initWorkspace(event.userId, workspaceDatabase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const radarService = new RadarService();
|
||||
|
||||
@@ -18,17 +18,70 @@ import { useAccount } from '@/renderer/contexts/account';
|
||||
import { ChevronsUpDown, LogOut, Plus, Settings } from 'lucide-react';
|
||||
import { useApp } from '@/renderer/contexts/app';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRadar } from '@/renderer/contexts/radar';
|
||||
|
||||
interface ReadStateIndicatorProps {
|
||||
importantCount: number;
|
||||
hasUnseenChanges: boolean;
|
||||
}
|
||||
|
||||
const ReadStateIndicator = ({
|
||||
importantCount,
|
||||
hasUnseenChanges,
|
||||
}: ReadStateIndicatorProps) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{importantCount > 0 && (
|
||||
<span className="mr-1 rounded-md px-1 py-0.5 text-xs bg-red-400 text-white">
|
||||
{importantCount}
|
||||
</span>
|
||||
)}
|
||||
{importantCount === 0 && hasUnseenChanges && (
|
||||
<span className="size-2 rounded-full bg-red-500" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export function LayoutSidebarFooter() {
|
||||
const app = useApp();
|
||||
const account = useAccount();
|
||||
const sidebar = useSidebar();
|
||||
const navigate = useNavigate();
|
||||
const radar = useRadar();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const otherAccounts = app.accounts.filter((a) => a.id !== account.id);
|
||||
|
||||
const accountStates: Record<string, ReadStateIndicatorProps> = {};
|
||||
for (const otherAccount of otherAccounts) {
|
||||
const accountWorkspaces = app.workspaces.filter(
|
||||
(w) => w.accountId === otherAccount.id
|
||||
);
|
||||
|
||||
const state: ReadStateIndicatorProps = {
|
||||
importantCount: 0,
|
||||
hasUnseenChanges: false,
|
||||
};
|
||||
|
||||
for (const accountWorkspace of accountWorkspaces) {
|
||||
const workspaceState = radar.getWorkspaceState(accountWorkspace.userId);
|
||||
state.importantCount += workspaceState.importantCount;
|
||||
state.hasUnseenChanges =
|
||||
state.hasUnseenChanges || workspaceState.hasUnseenChanges;
|
||||
}
|
||||
|
||||
accountStates[otherAccount.id] = state;
|
||||
}
|
||||
|
||||
const importantCount = Object.values(accountStates).reduce(
|
||||
(acc, curr) => acc + curr.importantCount,
|
||||
0
|
||||
);
|
||||
const hasUnseenChanges = Object.values(accountStates).some(
|
||||
(state) => state.hasUnseenChanges
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
@@ -49,6 +102,10 @@ export function LayoutSidebarFooter() {
|
||||
<span className="truncate text-xs">{account.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
<ReadStateIndicator
|
||||
importantCount={importantCount}
|
||||
hasUnseenChanges={hasUnseenChanges}
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -96,32 +153,39 @@ export function LayoutSidebarFooter() {
|
||||
<DropdownMenuLabel className="mb-1">
|
||||
Other accounts
|
||||
</DropdownMenuLabel>
|
||||
{otherAccounts.map((otherAccount) => (
|
||||
<DropdownMenuItem
|
||||
key={otherAccount.id}
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
navigate(`/${otherAccount.id}`);
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar
|
||||
className="h-8 w-8 rounded-lg"
|
||||
id={otherAccount.id}
|
||||
name={otherAccount.name}
|
||||
avatar={otherAccount.avatar}
|
||||
/>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{otherAccount.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{otherAccount.email}
|
||||
</span>
|
||||
{otherAccounts.map((otherAccount) => {
|
||||
const state = accountStates[otherAccount.id];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={otherAccount.id}
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
navigate(`/${otherAccount.id}`);
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar
|
||||
className="h-8 w-8 rounded-lg"
|
||||
id={otherAccount.id}
|
||||
name={otherAccount.name}
|
||||
avatar={otherAccount.avatar}
|
||||
/>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{otherAccount.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{otherAccount.email}
|
||||
</span>
|
||||
</div>
|
||||
<ReadStateIndicator
|
||||
importantCount={state.importantCount}
|
||||
hasUnseenChanges={state.hasUnseenChanges}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
|
||||
@@ -20,15 +20,15 @@ import {
|
||||
import { ChevronsUpDown, Settings, Plus, Bell } from 'lucide-react';
|
||||
import { useRadar } from '@/renderer/contexts/radar';
|
||||
|
||||
interface WorkspaceStateIndicatorProps {
|
||||
interface ReadStateIndicatorProps {
|
||||
importantCount: number;
|
||||
hasUnseenChanges: boolean;
|
||||
}
|
||||
|
||||
const WorkspaceStateIndicator = ({
|
||||
const ReadStateIndicator = ({
|
||||
importantCount,
|
||||
hasUnseenChanges,
|
||||
}: WorkspaceStateIndicatorProps) => {
|
||||
}: ReadStateIndicatorProps) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{importantCount > 0 && (
|
||||
@@ -85,7 +85,7 @@ export const LayoutSidebarHeader = () => {
|
||||
<span className="truncate text-xs">Free Plan</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
<WorkspaceStateIndicator
|
||||
<ReadStateIndicator
|
||||
importantCount={importantCount}
|
||||
hasUnseenChanges={hasUnseenChanges}
|
||||
/>
|
||||
@@ -152,7 +152,7 @@ export const LayoutSidebarHeader = () => {
|
||||
<p className="flex-1 text-left text-sm leading-tight truncate font-normal">
|
||||
{otherWorkspace.name}
|
||||
</p>
|
||||
<WorkspaceStateIndicator
|
||||
<ReadStateIndicator
|
||||
importantCount={workspaceState.importantCount}
|
||||
hasUnseenChanges={workspaceState.hasUnseenChanges}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user