mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 11:47:47 +01:00
Refactor app metadata
This commit is contained in:
@@ -5,7 +5,6 @@ import { createDebugger } from '@colanode/core';
|
||||
import { app, BrowserWindow, ipcMain, protocol, shell } from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
import { WindowSize } from '@/shared/types/metadata';
|
||||
import { mediator } from '@/main/mediator';
|
||||
import { getAppIconPath } from '@/main/lib/utils';
|
||||
import { CommandInput, CommandMap } from '@/shared/commands';
|
||||
@@ -44,7 +43,7 @@ const createWindow = async () => {
|
||||
await appService.migrate();
|
||||
|
||||
// Create the browser window.
|
||||
let windowSize = await appService.metadata.get<WindowSize>('window_size');
|
||||
let windowSize = (await appService.metadata.get('window_size'))?.value;
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: windowSize?.width ?? 1200,
|
||||
height: windowSize?.height ?? 800,
|
||||
|
||||
@@ -38,20 +38,20 @@ interface DeletedTokenTable {
|
||||
created_at: ColumnType<string, string, string>;
|
||||
}
|
||||
|
||||
interface MetadataTable {
|
||||
interface AppMetadataTable {
|
||||
key: ColumnType<string, string, never>;
|
||||
value: ColumnType<string, string, string>;
|
||||
created_at: ColumnType<string, string, never>;
|
||||
updated_at: ColumnType<string | null, string | null, string | null>;
|
||||
}
|
||||
|
||||
export type SelectMetadata = Selectable<MetadataTable>;
|
||||
export type CreateMetadata = Insertable<MetadataTable>;
|
||||
export type UpdateMetadata = Updateable<MetadataTable>;
|
||||
export type SelectAppMetadata = Selectable<AppMetadataTable>;
|
||||
export type CreateAppMetadata = Insertable<AppMetadataTable>;
|
||||
export type UpdateAppMetadata = Updateable<AppMetadataTable>;
|
||||
|
||||
export interface AppDatabaseSchema {
|
||||
servers: ServerTable;
|
||||
accounts: AccountTable;
|
||||
deleted_tokens: DeletedTokenTable;
|
||||
metadata: MetadataTable;
|
||||
metadata: AppMetadataTable;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
} from '@colanode/core';
|
||||
import { encodeState } from '@colanode/crdt';
|
||||
|
||||
import { SelectAccount, SelectServer } from '@/main/databases/app';
|
||||
import {
|
||||
SelectAccount,
|
||||
SelectAppMetadata,
|
||||
SelectServer,
|
||||
} from '@/main/databases/app';
|
||||
import { SelectEmoji } from '@/main/databases/emojis';
|
||||
import { SelectIcon } from '@/main/databases/icons';
|
||||
import { SelectWorkspace } from '@/main/databases/account';
|
||||
@@ -41,6 +45,7 @@ import {
|
||||
import { EntryInteraction } from '@/shared/types/entries';
|
||||
import { Emoji } from '@/shared/types/emojis';
|
||||
import { Icon } from '@/shared/types/icons';
|
||||
import { AppMetadata, AppMetadataKey } from '@/shared/types/apps';
|
||||
|
||||
export const mapUser = (row: SelectUser): User => {
|
||||
return {
|
||||
@@ -291,6 +296,14 @@ export const mapIcon = (row: SelectIcon): Icon => {
|
||||
};
|
||||
};
|
||||
|
||||
export const mapAppMetadata = (row: SelectAppMetadata): AppMetadata => {
|
||||
return {
|
||||
key: row.key as AppMetadataKey,
|
||||
value: JSON.parse(row.value),
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
};
|
||||
export const mapWorkspaceMetadata = (
|
||||
row: SelectWorkspaceMetadata
|
||||
): WorkspaceMetadata => {
|
||||
|
||||
66
apps/desktop/src/main/queries/apps/app-metadata-list.ts
Normal file
66
apps/desktop/src/main/queries/apps/app-metadata-list.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
|
||||
import { mapAppMetadata } from '@/main/lib/mappers';
|
||||
import { AppMetadataListQueryInput } from '@/shared/queries/apps/app-metadata-list';
|
||||
import { Event } from '@/shared/types/events';
|
||||
import { AppMetadata } from '@/shared/types/apps';
|
||||
import { SelectAppMetadata } from '@/main/databases/app/schema';
|
||||
import { appService } from '@/main/services/app-service';
|
||||
|
||||
export class AppMetadataListQueryHandler
|
||||
implements QueryHandler<AppMetadataListQueryInput>
|
||||
{
|
||||
public async handleQuery(
|
||||
_: AppMetadataListQueryInput
|
||||
): Promise<AppMetadata[]> {
|
||||
const rows = await this.getAppMetadata();
|
||||
if (!rows) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return rows.map(mapAppMetadata);
|
||||
}
|
||||
|
||||
public async checkForChanges(
|
||||
event: Event,
|
||||
_: AppMetadataListQueryInput,
|
||||
output: AppMetadata[]
|
||||
): Promise<ChangeCheckResult<AppMetadataListQueryInput>> {
|
||||
if (event.type === 'app_metadata_updated') {
|
||||
const newOutput = output.map((metadata) => {
|
||||
if (metadata.key === event.metadata.key) {
|
||||
return event.metadata;
|
||||
}
|
||||
return metadata;
|
||||
});
|
||||
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: newOutput,
|
||||
};
|
||||
}
|
||||
|
||||
if (event.type === 'app_metadata_deleted') {
|
||||
const newOutput = output.filter(
|
||||
(metadata) => metadata.key !== event.metadata.key
|
||||
);
|
||||
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: newOutput,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasChanges: false,
|
||||
};
|
||||
}
|
||||
|
||||
private async getAppMetadata(): Promise<SelectAppMetadata[] | undefined> {
|
||||
const rows = await appService.database
|
||||
.selectFrom('metadata')
|
||||
.selectAll()
|
||||
.execute();
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AppMetadataListQueryHandler } from '@/main/queries/apps/app-metadata-list';
|
||||
import { AccountGetQueryHandler } from '@/main/queries/accounts/account-get';
|
||||
import { AccountListQueryHandler } from '@/main/queries/accounts/accounts-list';
|
||||
import { EmojiGetQueryHandler } from '@/main/queries/emojis/emoji-get';
|
||||
@@ -41,6 +42,7 @@ type QueryHandlerMap = {
|
||||
};
|
||||
|
||||
export const queryHandlerMap: QueryHandlerMap = {
|
||||
app_metadata_list: new AppMetadataListQueryHandler(),
|
||||
account_list: new AccountListQueryHandler(),
|
||||
message_list: new MessageListQueryHandler(),
|
||||
message_reaction_list: new MessageReactionsListQueryHandler(),
|
||||
|
||||
@@ -18,6 +18,7 @@ import { EventLoop } from '@/main/lib/event-loop';
|
||||
import { parseApiError } from '@/shared/lib/axios';
|
||||
import { NotificationService } from '@/main/services/notification-service';
|
||||
import { eventBus } from '@/shared/lib/event-bus';
|
||||
import { AppPlatform } from '@/shared/types/apps';
|
||||
|
||||
export class AppService {
|
||||
private readonly debug = createDebugger('desktop:service:app');
|
||||
@@ -74,6 +75,9 @@ export class AppService {
|
||||
});
|
||||
|
||||
await migrator.migrateToLatest();
|
||||
|
||||
await this.metadata.set('version', this.version);
|
||||
await this.metadata.set('platform', process.platform as AppPlatform);
|
||||
}
|
||||
|
||||
public getAccount(id: string): AccountService | null {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { createDebugger } from '@colanode/core';
|
||||
|
||||
import { AppService } from '@/main/services/app-service';
|
||||
import {
|
||||
AppMetadata,
|
||||
AppMetadataMap,
|
||||
AppMetadataKey,
|
||||
} from '@/shared/types/apps';
|
||||
import { mapAppMetadata } from '@/main/lib/mappers';
|
||||
import { eventBus } from '@/shared/lib/event-bus';
|
||||
|
||||
export class MetadataService {
|
||||
private readonly debug = createDebugger('desktop:service:metadata');
|
||||
@@ -10,7 +17,18 @@ export class MetadataService {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
public async get<T>(key: string): Promise<T | null> {
|
||||
public async getAll(): Promise<AppMetadata[]> {
|
||||
const metadata = await this.app.database
|
||||
.selectFrom('metadata')
|
||||
.selectAll()
|
||||
.execute();
|
||||
|
||||
return metadata.map(mapAppMetadata);
|
||||
}
|
||||
|
||||
public async get<K extends AppMetadataKey>(
|
||||
key: K
|
||||
): Promise<AppMetadataMap[K] | null> {
|
||||
const metadata = await this.app.database
|
||||
.selectFrom('metadata')
|
||||
.selectAll()
|
||||
@@ -21,14 +39,18 @@ export class MetadataService {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(metadata.value) as T;
|
||||
return mapAppMetadata(metadata) as AppMetadataMap[K];
|
||||
}
|
||||
|
||||
public async set<T>(key: string, value: T) {
|
||||
public async set<K extends AppMetadataKey>(
|
||||
key: K,
|
||||
value: AppMetadataMap[K]['value']
|
||||
) {
|
||||
this.debug(`Setting metadata key ${key} to value ${value}`);
|
||||
|
||||
await this.app.database
|
||||
const createdMetadata = await this.app.database
|
||||
.insertInto('metadata')
|
||||
.returningAll()
|
||||
.values({
|
||||
key,
|
||||
value: JSON.stringify(value),
|
||||
@@ -40,15 +62,34 @@ export class MetadataService {
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
)
|
||||
.execute();
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!createdMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventBus.publish({
|
||||
type: 'app_metadata_updated',
|
||||
metadata: mapAppMetadata(createdMetadata),
|
||||
});
|
||||
}
|
||||
|
||||
public async delete(key: string) {
|
||||
this.debug(`Deleting metadata key ${key}`);
|
||||
|
||||
await this.app.database
|
||||
const deletedMetadata = await this.app.database
|
||||
.deleteFrom('metadata')
|
||||
.where('key', '=', key)
|
||||
.execute();
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!deletedMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventBus.publish({
|
||||
type: 'app_metadata_deleted',
|
||||
metadata: mapAppMetadata(deletedMetadata),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Spinner } from '@/renderer/components/ui/spinner';
|
||||
|
||||
export const RootLoader = () => {
|
||||
export const AppLoader = () => {
|
||||
return (
|
||||
<div className="min-w-screen flex h-full min-h-screen w-full items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-8 text-center">
|
||||
@@ -1,11 +1,45 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
|
||||
import { AppContext } from './contexts/app';
|
||||
|
||||
import { DelayedComponent } from '@/renderer/components/ui/delayed-component';
|
||||
import { AppLoader } from '@/renderer/app-loader';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import { RadarProvider } from '@/renderer/radar-provider';
|
||||
|
||||
export const App = () => {
|
||||
const [initialized, setInitialized] = React.useState(false);
|
||||
|
||||
const { data, isPending } = useQuery({
|
||||
type: 'app_metadata_list',
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
window.colanode.init().then(() => {
|
||||
setInitialized(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!initialized || isPending) {
|
||||
return (
|
||||
<DelayedComponent>
|
||||
<AppLoader />
|
||||
</DelayedComponent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RadarProvider>
|
||||
<Outlet />
|
||||
</RadarProvider>
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
getMetadata: (key: string) => {
|
||||
return data?.find((metadata) => metadata.key === key)?.value;
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RadarProvider>
|
||||
<Outlet />
|
||||
</RadarProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SidebarMenuIcon } from '@/renderer/components/layouts/sidebars/sidebar-
|
||||
import { SidebarMenuHeader } from '@/renderer/components/layouts/sidebars/sidebar-menu-header';
|
||||
import { SidebarMenuFooter } from '@/renderer/components/layouts/sidebars/sidebar-menu-footer';
|
||||
import { SidebarMenuType } from '@/shared/types/workspaces';
|
||||
import { useApp } from '@/renderer/contexts/app';
|
||||
|
||||
interface SidebarMenuProps {
|
||||
value: SidebarMenuType;
|
||||
@@ -11,13 +12,22 @@ interface SidebarMenuProps {
|
||||
}
|
||||
|
||||
export const SidebarMenu = ({ value, onChange }: SidebarMenuProps) => {
|
||||
const app = useApp();
|
||||
const platform = app.getMetadata('platform');
|
||||
const windowSize = app.getMetadata('window_size');
|
||||
const showMacOsPlaceholder = platform === 'darwin' && !windowSize?.fullscreen;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-[65px] min-w-[65px] items-center bg-slate-100">
|
||||
<div className="w-full h-8 flex gap-[8px] px-[6px] py-[7px]">
|
||||
<div className="w-3 h-3 bg-gray-400 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-gray-400 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-gray-400 rounded-full"></div>
|
||||
</div>
|
||||
{showMacOsPlaceholder ? (
|
||||
<div className="w-full h-8 flex gap-[8px] px-[6px] py-[7px]">
|
||||
<div className="w-3 h-3 bg-gray-400 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-gray-400 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-gray-400 rounded-full"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-4" />
|
||||
)}
|
||||
<SidebarMenuHeader />
|
||||
<div className="flex flex-col gap-1 mt-2 w-full p-2 items-center flex-grow">
|
||||
<SidebarMenuIcon
|
||||
|
||||
13
apps/desktop/src/renderer/contexts/app.ts
Normal file
13
apps/desktop/src/renderer/contexts/app.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import { AppMetadataKey, AppMetadataMap } from '@/shared/types/apps';
|
||||
|
||||
interface AppContext {
|
||||
getMetadata: <K extends AppMetadataKey>(
|
||||
key: K
|
||||
) => AppMetadataMap[K]['value'] | undefined;
|
||||
}
|
||||
|
||||
export const AppContext = createContext<AppContext>({} as AppContext);
|
||||
|
||||
export const useApp = () => useContext(AppContext);
|
||||
@@ -11,14 +11,12 @@ import { App } from '@/renderer/app';
|
||||
import { Account } from '@/renderer/components/accounts/account';
|
||||
import { AccountRedirect } from '@/renderer/components/accounts/account-redirect';
|
||||
import { Login } from '@/renderer/components/accounts/login';
|
||||
import { DelayedComponent } from '@/renderer/components/ui/delayed-component';
|
||||
import { Toaster } from '@/renderer/components/ui/toaster';
|
||||
import { TooltipProvider } from '@/renderer/components/ui/tooltip';
|
||||
import { Workspace } from '@/renderer/components/workspaces/workspace';
|
||||
import { WorkspaceCreate } from '@/renderer/components/workspaces/workspace-create';
|
||||
import { WorkspaceRedirect } from '@/renderer/components/workspaces/workspace-redirect';
|
||||
import { useEventBus } from '@/renderer/hooks/use-event-bus';
|
||||
import { RootLoader } from '@/renderer/root-loader';
|
||||
import { Event } from '@/shared/types/events';
|
||||
|
||||
const router = createHashRouter([
|
||||
@@ -56,7 +54,7 @@ const router = createHashRouter([
|
||||
},
|
||||
]);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
networkMode: 'always',
|
||||
@@ -69,7 +67,6 @@ const queryClient = new QueryClient({
|
||||
|
||||
const Root = () => {
|
||||
const eventBus = useEventBus();
|
||||
const [initialized, setInitialized] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const id = eventBus.subscribe((event: Event) => {
|
||||
@@ -96,23 +93,11 @@ const Root = () => {
|
||||
}
|
||||
});
|
||||
|
||||
window.colanode.init().then(() => {
|
||||
setInitialized(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
eventBus.unsubscribe(id);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!initialized) {
|
||||
return (
|
||||
<DelayedComponent>
|
||||
<RootLoader />
|
||||
</DelayedComponent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
|
||||
14
apps/desktop/src/shared/queries/apps/app-metadata-list.ts
Normal file
14
apps/desktop/src/shared/queries/apps/app-metadata-list.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { AppMetadata } from '@/shared/types/apps';
|
||||
|
||||
export type AppMetadataListQueryInput = {
|
||||
type: 'app_metadata_list';
|
||||
};
|
||||
|
||||
declare module '@/shared/queries' {
|
||||
interface QueryMap {
|
||||
app_metadata_list: {
|
||||
input: AppMetadataListQueryInput;
|
||||
output: AppMetadata[];
|
||||
};
|
||||
}
|
||||
}
|
||||
48
apps/desktop/src/shared/types/apps.ts
Normal file
48
apps/desktop/src/shared/types/apps.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export type AppPlatform =
|
||||
| 'aix'
|
||||
| 'darwin'
|
||||
| 'freebsd'
|
||||
| 'linux'
|
||||
| 'openbsd'
|
||||
| 'sunos'
|
||||
| 'win32';
|
||||
|
||||
export type WindowSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
fullscreen: boolean;
|
||||
};
|
||||
|
||||
export type AppPlatformMetadata = {
|
||||
key: 'platform';
|
||||
value: AppPlatform;
|
||||
createdAt: string;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type AppVersionMetadata = {
|
||||
key: 'version';
|
||||
value: string;
|
||||
createdAt: string;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type AppWindowSizeMetadata = {
|
||||
key: 'window_size';
|
||||
value: WindowSize;
|
||||
createdAt: string;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type AppMetadata =
|
||||
| AppPlatformMetadata
|
||||
| AppVersionMetadata
|
||||
| AppWindowSizeMetadata;
|
||||
|
||||
export type AppMetadataKey = AppMetadata['key'];
|
||||
|
||||
export type AppMetadataMap = {
|
||||
platform: AppPlatformMetadata;
|
||||
version: AppVersionMetadata;
|
||||
window_size: AppWindowSizeMetadata;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Entry, Message } from '@colanode/core';
|
||||
|
||||
import { AppMetadata } from '@/shared/types/apps';
|
||||
import { EntryInteraction } from '@/shared/types/entries';
|
||||
import {
|
||||
MessageInteraction,
|
||||
@@ -238,6 +239,16 @@ export type AccountConnectionMessageEvent = {
|
||||
message: Message;
|
||||
};
|
||||
|
||||
export type AppMetadataUpdatedEvent = {
|
||||
type: 'app_metadata_updated';
|
||||
metadata: AppMetadata;
|
||||
};
|
||||
|
||||
export type AppMetadataDeletedEvent = {
|
||||
type: 'app_metadata_deleted';
|
||||
metadata: AppMetadata;
|
||||
};
|
||||
|
||||
export type WorkspaceMetadataUpdatedEvent = {
|
||||
type: 'workspace_metadata_updated';
|
||||
accountId: string;
|
||||
@@ -289,5 +300,7 @@ export type Event =
|
||||
| AccountConnectionOpenedEvent
|
||||
| AccountConnectionClosedEvent
|
||||
| AccountConnectionMessageEvent
|
||||
| AppMetadataUpdatedEvent
|
||||
| AppMetadataDeletedEvent
|
||||
| WorkspaceMetadataUpdatedEvent
|
||||
| WorkspaceMetadataDeletedEvent;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export type WindowSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
fullscreen: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user