diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 552818f4..a09c23c0 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -186,6 +186,10 @@ const initApp = async (): Promise => { await app.metadata.delete('app', 'theme.mode'); } + // add default Colanode servers + await app.createServer(new URL('https://eu.colanode.com/config')); + await app.createServer(new URL('https://us.colanode.com/config')); + return 'success'; }; diff --git a/apps/web/src/workers/dedicated.ts b/apps/web/src/workers/dedicated.ts index d7ebd02e..83397dd8 100644 --- a/apps/web/src/workers/dedicated.ts +++ b/apps/web/src/workers/dedicated.ts @@ -9,7 +9,13 @@ import { import { QueryInput, QueryMap } from '@colanode/client/queries'; import { AppMeta, AppService } from '@colanode/client/services'; import { AppInitOutput } from '@colanode/client/types'; -import { build, extractFileSubtype, generateId, IdType } from '@colanode/core'; +import { + build, + extractFileSubtype, + generateId, + IdType, + isColanodeDomain, +} from '@colanode/core'; import { BroadcastInitMessage, BroadcastMessage, @@ -72,6 +78,12 @@ navigator.locks.request('colanode', async () => { await app.metadata.set('app', 'version', build.version); await app.metadata.set('app', 'platform', appMeta.platform); + const domain = self.location.hostname; + if (isColanodeDomain(domain)) { + await app.createServer(new URL('https://eu.colanode.com/config')); + await app.createServer(new URL('https://us.colanode.com/config')); + } + appInitOutput = 'success'; broadcastMessage({ diff --git a/packages/client/src/handlers/mutations/index.ts b/packages/client/src/handlers/mutations/index.ts index a97decad..3f3272b6 100644 --- a/packages/client/src/handlers/mutations/index.ts +++ b/packages/client/src/handlers/mutations/index.ts @@ -62,6 +62,7 @@ import { RecordFieldValueSetMutationHandler } from './records/record-field-value import { RecordNameUpdateMutationHandler } from './records/record-name-update'; import { ServerCreateMutationHandler } from './servers/server-create'; import { ServerDeleteMutationHandler } from './servers/server-delete'; +import { ServerSyncMutationHandler } from './servers/server-sync'; import { SpaceChildReorderMutationHandler } from './spaces/space-child-reorder'; import { SpaceCreateMutationHandler } from './spaces/space-create'; import { SpaceDeleteMutationHandler } from './spaces/space-delete'; @@ -121,6 +122,7 @@ export const buildMutationHandlerMap = ( 'select.option.update': new SelectOptionUpdateMutationHandler(app), 'server.create': new ServerCreateMutationHandler(app), 'server.delete': new ServerDeleteMutationHandler(app), + 'server.sync': new ServerSyncMutationHandler(app), 'space.create': new SpaceCreateMutationHandler(app), 'space.delete': new SpaceDeleteMutationHandler(app), 'user.role.update': new UserRoleUpdateMutationHandler(app), diff --git a/packages/client/src/handlers/mutations/servers/server-delete.ts b/packages/client/src/handlers/mutations/servers/server-delete.ts index 54829481..5fe76d56 100644 --- a/packages/client/src/handlers/mutations/servers/server-delete.ts +++ b/packages/client/src/handlers/mutations/servers/server-delete.ts @@ -5,7 +5,7 @@ import { ServerDeleteMutationOutput, } from '@colanode/client/mutations/servers/server-delete'; import { AppService } from '@colanode/client/services/app-service'; -import { isColanodeServer } from '@colanode/core'; +import { isColanodeDomain } from '@colanode/core'; export class ServerDeleteMutationHandler implements MutationHandler @@ -19,7 +19,7 @@ export class ServerDeleteMutationHandler async handleMutation( input: ServerDeleteMutationInput ): Promise { - if (isColanodeServer(input.domain)) { + if (isColanodeDomain(input.domain)) { throw new MutationError( MutationErrorCode.ServerDeleteForbidden, 'Cannot delete Colanode server' diff --git a/packages/client/src/handlers/mutations/servers/server-sync.ts b/packages/client/src/handlers/mutations/servers/server-sync.ts new file mode 100644 index 00000000..3973729e --- /dev/null +++ b/packages/client/src/handlers/mutations/servers/server-sync.ts @@ -0,0 +1,44 @@ +import ms from 'ms'; + +import { MutationHandler } from '@colanode/client/lib/types'; +import { MutationError, MutationErrorCode } from '@colanode/client/mutations'; +import { + ServerSyncMutationInput, + ServerSyncMutationOutput, +} from '@colanode/client/mutations/servers/server-sync'; +import { AppService } from '@colanode/client/services/app-service'; + +export class ServerSyncMutationHandler + implements MutationHandler +{ + private readonly app: AppService; + + constructor(app: AppService) { + this.app = app; + } + + async handleMutation( + input: ServerSyncMutationInput + ): Promise { + const server = this.app.getServer(input.domain); + if (!server) { + throw new MutationError( + MutationErrorCode.ServerNotFound, + `Server ${input.domain} was not found! Try using a different server.` + ); + } + + // no need to sync if the server has been synced in the last minute + const lastSyncedAt = server.server.syncedAt; + if (lastSyncedAt && lastSyncedAt.getTime() > Date.now() - ms('1 minute')) { + return { + success: true, + }; + } + + const success = await server.sync(); + return { + success, + }; + } +} diff --git a/packages/client/src/jobs/server-sync.ts b/packages/client/src/jobs/server-sync.ts index c6772046..4c9f796b 100644 --- a/packages/client/src/jobs/server-sync.ts +++ b/packages/client/src/jobs/server-sync.ts @@ -1,3 +1,5 @@ +import ms from 'ms'; + import { JobHandler, JobOutput, @@ -38,6 +40,20 @@ export class ServerSyncJobHandler implements JobHandler { }; } + const accounts = this.app + .getAccounts() + .filter((account) => account.server.domain === server.domain); + + if (accounts.length === 0) { + // don't sync if the server has no active accounts and has been synced in the last day + const lastSyncedAt = server.server.syncedAt; + if (lastSyncedAt && lastSyncedAt.getTime() > Date.now() - ms('1 day')) { + return { + type: 'success', + }; + } + } + await server.sync(); return { type: 'success', diff --git a/packages/client/src/mutations/index.ts b/packages/client/src/mutations/index.ts index e43f6427..e77b249a 100644 --- a/packages/client/src/mutations/index.ts +++ b/packages/client/src/mutations/index.ts @@ -67,6 +67,7 @@ export * from './users/users-create'; export * from './files/temp-file-create'; export * from './apps/tab-create'; export * from './apps/tab-update'; +export * from './servers/server-sync'; export * from './apps/tab-delete'; // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/packages/client/src/mutations/servers/server-sync.ts b/packages/client/src/mutations/servers/server-sync.ts new file mode 100644 index 00000000..645215a0 --- /dev/null +++ b/packages/client/src/mutations/servers/server-sync.ts @@ -0,0 +1,17 @@ +export type ServerSyncMutationInput = { + type: 'server.sync'; + domain: string; +}; + +export type ServerSyncMutationOutput = { + success: boolean; +}; + +declare module '@colanode/client/mutations' { + interface MutationMap { + 'server.sync': { + input: ServerSyncMutationInput; + output: ServerSyncMutationOutput; + }; + } +} diff --git a/packages/client/src/services/server-service.ts b/packages/client/src/services/server-service.ts index b19dd907..bca4fd1a 100644 --- a/packages/client/src/services/server-service.ts +++ b/packages/client/src/services/server-service.ts @@ -97,7 +97,7 @@ export class ServerService { await this.app.jobs.triggerJobSchedule(scheduleId); } - public async sync() { + public async sync(): Promise { const config = await ServerService.fetchServerConfig(this.configUrl); if (config) { const attributes: ServerAttributes = { @@ -158,6 +158,8 @@ export class ServerService { type: 'server.updated', server: this.server, }); + + return isAvailable; } public static async fetchServerConfig(configUrl: URL | string) { diff --git a/packages/core/src/lib/servers.ts b/packages/core/src/lib/servers.ts index 2174fcc8..25739838 100644 --- a/packages/core/src/lib/servers.ts +++ b/packages/core/src/lib/servers.ts @@ -1,3 +1,3 @@ -export const isColanodeServer = (domain: string) => { +export const isColanodeDomain = (domain: string) => { return domain.endsWith('.colanode.com'); }; diff --git a/packages/ui/src/components/auth/auth-server.tsx b/packages/ui/src/components/auth/auth-server.tsx index 68e797ed..9b540d13 100644 --- a/packages/ui/src/components/auth/auth-server.tsx +++ b/packages/ui/src/components/auth/auth-server.tsx @@ -1,13 +1,11 @@ import { useLiveQuery } from '@tanstack/react-db'; -import { PlusIcon, SettingsIcon } from 'lucide-react'; +import { PlusIcon } from 'lucide-react'; import { useState } from 'react'; import { Server } from '@colanode/client/types'; import { collections } from '@colanode/ui/collections'; -import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar'; +import { ServerCard } from '@colanode/ui/components/servers/server-card'; import { ServerCreateDialog } from '@colanode/ui/components/servers/server-create-dialog'; -import { ServerDeleteDialog } from '@colanode/ui/components/servers/server-delete-dialog'; -import { ServerSettingsDialog } from '@colanode/ui/components/servers/server-settings-dialog'; interface AuthServerProps { onSelect: (server: Server) => void; @@ -15,16 +13,11 @@ interface AuthServerProps { export const AuthServer = ({ onSelect }: AuthServerProps) => { const [openCreate, setOpenCreate] = useState(false); - const [settingsDomain, setSettingsDomain] = useState(null); - const [deleteDomain, setDeleteDomain] = useState(null); + const serversQuery = useLiveQuery((q) => q.from({ servers: collections.servers }) ); const servers = serversQuery.data ?? []; - const settingsServer = servers.find( - (server) => server.domain === settingsDomain - ); - const deleteServer = servers.find((server) => server.domain === deleteDomain); return (
@@ -40,31 +33,7 @@ export const AuthServer = ({ onSelect }: AuthServerProps) => {
{servers.map((server) => ( - - + ))}
); }; diff --git a/packages/ui/src/components/servers/server-card.tsx b/packages/ui/src/components/servers/server-card.tsx new file mode 100644 index 00000000..8ee4509f --- /dev/null +++ b/packages/ui/src/components/servers/server-card.tsx @@ -0,0 +1,87 @@ +import { SettingsIcon } from 'lucide-react'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +import { Server } from '@colanode/client/types'; +import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar'; +import { ServerDeleteDialog } from '@colanode/ui/components/servers/server-delete-dialog'; +import { ServerSettingsDialog } from '@colanode/ui/components/servers/server-settings-dialog'; +import { Spinner } from '@colanode/ui/components/ui/spinner'; +import { useMutation } from '@colanode/ui/hooks/use-mutation'; + +interface ServerCardProps { + server: Server; + onSelect: (server: Server) => void; +} + +export const ServerCard = ({ server, onSelect }: ServerCardProps) => { + const [openSettings, setOpenSettings] = useState(false); + const [openDelete, setOpenDelete] = useState(false); + const { mutate: syncServer, isPending: isSyncing } = useMutation(); + + const handleServerClick = () => { + if (isSyncing) return; + + syncServer({ + input: { + type: 'server.sync', + domain: server.domain, + }, + onSuccess() { + onSelect(server); + }, + onError(error) { + toast.error(error.message); + }, + }); + }; + + return ( + <> + + + { + setOpenSettings(false); + setOpenDelete(true); + }} + /> + + + ); +}; diff --git a/packages/ui/src/components/servers/server-settings-dialog.tsx b/packages/ui/src/components/servers/server-settings-dialog.tsx index af2af448..f3a556d2 100644 --- a/packages/ui/src/components/servers/server-settings-dialog.tsx +++ b/packages/ui/src/components/servers/server-settings-dialog.tsx @@ -2,7 +2,7 @@ import { TrashIcon } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Server } from '@colanode/client/types'; -import { formatDate, isColanodeServer, timeAgo } from '@colanode/core'; +import { formatDate, isColanodeDomain, timeAgo } from '@colanode/core'; import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar'; import { Badge } from '@colanode/ui/components/ui/badge'; import { Button } from '@colanode/ui/components/ui/button'; @@ -45,7 +45,7 @@ export const ServerSettingsDialog = ({ return () => clearInterval(interval); }, [open]); - const canDelete = !isColanodeServer(server.domain); + const canDelete = !isColanodeDomain(server.domain); const isAvailable = server.state?.isAvailable ?? false; const isOutdated = server.isOutdated; diff --git a/packages/ui/src/components/workspaces/storage/workspace-storage-cloud.tsx b/packages/ui/src/components/workspaces/storage/workspace-storage-cloud.tsx index d9d2f499..0cf80922 100644 --- a/packages/ui/src/components/workspaces/storage/workspace-storage-cloud.tsx +++ b/packages/ui/src/components/workspaces/storage/workspace-storage-cloud.tsx @@ -1,6 +1,6 @@ import { Cloud, ExternalLink } from 'lucide-react'; -import { isColanodeServer } from '@colanode/core'; +import { isColanodeDomain } from '@colanode/core'; import { Button } from '@colanode/ui/components/ui/button'; import { useServer } from '@colanode/ui/contexts/server'; @@ -8,7 +8,7 @@ const CLOUD_URL = 'https://cloud.colanode.com'; export const WorkspaceStorageCloud = () => { const server = useServer(); - if (!isColanodeServer(server.domain)) { + if (!isColanodeDomain(server.domain)) { return null; }