feat(core): add open link in app to doc menu (#8597)

add "open in desktop app" menu item for editor

fix AF-1547
This commit is contained in:
pengx17
2024-10-29 05:49:49 +00:00
committed by Peng Xiao
parent 5690720652
commit 835fdc33c0
21 changed files with 222 additions and 117 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ declare global {
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var __appInfo: { var __appInfo: {
electron: boolean; electron: boolean;
schema: string; scheme: string;
windowName: string; windowName: string;
}; };
} }
@@ -11,7 +11,7 @@ import { I18nProvider } from '@affine/core/modules/i18n';
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage'; import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
import { CustomThemeModifier } from '@affine/core/modules/theme-editor'; import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
import { import {
ClientSchemaProvider, ClientSchemeProvider,
PopupWindowProvider, PopupWindowProvider,
} from '@affine/core/modules/url'; } from '@affine/core/modules/url';
import { configureSqliteUserspaceStorageProvider } from '@affine/core/modules/userspace'; import { configureSqliteUserspaceStorageProvider } from '@affine/core/modules/userspace';
@@ -71,9 +71,9 @@ framework.impl(PopupWindowProvider, {
}); });
}, },
}); });
framework.impl(ClientSchemaProvider, { framework.impl(ClientSchemeProvider, {
getClientSchema() { getClientScheme() {
return appInfo?.schema; return appInfo?.scheme;
}, },
}); });
framework.impl(ValidatorProvider, { framework.impl(ValidatorProvider, {
@@ -6,6 +6,7 @@ import { buildType, isDev } from './config';
import { logger } from './logger'; import { logger } from './logger';
import { uiSubjects } from './ui'; import { uiSubjects } from './ui';
import { import {
addTab,
getMainWindow, getMainWindow,
openUrlInHiddenWindow, openUrlInHiddenWindow,
openUrlInMainWindow, openUrlInMainWindow,
@@ -81,6 +82,24 @@ async function handleAffineUrl(url: string) {
method, method,
payload, payload,
}); });
} else if (
urlObj.searchParams.get('new-tab') &&
urlObj.pathname.startsWith('/workspace')
) {
// @todo(@forehalo): refactor router utilities
// basename of /workspace/xxx/yyy is /workspace/xxx
const basename = urlObj.pathname.split('/').slice(0, 3).join('/');
const pathname = '/' + urlObj.pathname.split('/').slice(3).join('/');
await addTab({
basename,
show: true,
view: {
path: {
pathname: pathname,
},
},
});
} else { } else {
const hiddenWindow = urlObj.searchParams.get('hidden') const hiddenWindow = urlObj.searchParams.get('hidden')
? await openUrlInHiddenWindow(urlObj) ? await openUrlInHiddenWindow(urlObj)
@@ -41,9 +41,9 @@ const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']);
const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase(); const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase();
const buildType = ReleaseTypeSchema.parse(envBuildType); const buildType = ReleaseTypeSchema.parse(envBuildType);
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'development';
let schema = let scheme =
buildType === 'stable' ? 'affine' : (`affine-${envBuildType}` as Schema); buildType === 'stable' ? 'affine' : (`affine-${envBuildType}` as Schema);
schema = isDev ? 'affine-dev' : schema; scheme = isDev ? 'affine-dev' : scheme;
export const appInfo = { export const appInfo = {
electron: true, electron: true,
@@ -53,7 +53,7 @@ export const appInfo = {
viewId: viewId:
process.argv.find(arg => arg.startsWith('--view-id='))?.split('=')[1] ?? process.argv.find(arg => arg.startsWith('--view-id='))?.split('=')[1] ??
'unknown', 'unknown',
schema, scheme,
}; };
function getMainAPIs() { function getMainAPIs() {
+4 -6
View File
@@ -7,10 +7,8 @@ import { configureCommonModules } from '@affine/core/modules';
import { AuthService, WebSocketAuthProvider } from '@affine/core/modules/cloud'; import { AuthService, WebSocketAuthProvider } from '@affine/core/modules/cloud';
import { I18nProvider } from '@affine/core/modules/i18n'; import { I18nProvider } from '@affine/core/modules/i18n';
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage'; import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
import { import { PopupWindowProvider } from '@affine/core/modules/url';
ClientSchemaProvider, import { ClientSchemeProvider } from '@affine/core/modules/url/providers/client-schema';
PopupWindowProvider,
} from '@affine/core/modules/url';
import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace'; import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace';
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench'; import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
import { import {
@@ -51,8 +49,8 @@ framework.impl(PopupWindowProvider, {
}).catch(console.error); }).catch(console.error);
}, },
}); });
framework.impl(ClientSchemaProvider, { framework.impl(ClientSchemeProvider, {
getClientSchema() { getClientScheme() {
return 'affine'; return 'affine';
}, },
}); });
@@ -35,7 +35,7 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
const oauthProviders = useLiveData( const oauthProviders = useLiveData(
serverConfig.config$.map(r => r?.oauthProviders) serverConfig.config$.map(r => r?.oauthProviders)
); );
const schema = urlService.getClientSchema(); const scheme = urlService.getClientScheme();
if (!oauth) { if (!oauth) {
return <Skeleton height={50} />; return <Skeleton height={50} />;
@@ -46,7 +46,7 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
key={provider} key={provider}
provider={provider} provider={provider}
redirectUrl={redirectUrl} redirectUrl={redirectUrl}
schema={schema} scheme={scheme}
popupWindow={url => { popupWindow={url => {
urlService.openPopupWindow(url); urlService.openPopupWindow(url);
}} }}
@@ -57,12 +57,12 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
function OAuthProvider({ function OAuthProvider({
provider, provider,
redirectUrl, redirectUrl,
schema, scheme,
popupWindow, popupWindow,
}: { }: {
provider: OAuthProviderType; provider: OAuthProviderType;
redirectUrl?: string; redirectUrl?: string;
schema?: string; scheme?: string;
popupWindow: (url: string) => void; popupWindow: (url: string) => void;
}) { }) {
const { icon } = OAuthProviderMap[provider]; const { icon } = OAuthProviderMap[provider];
@@ -76,8 +76,8 @@ function OAuthProvider({
params.set('redirect_uri', redirectUrl); params.set('redirect_uri', redirectUrl);
} }
if (schema) { if (scheme) {
params.set('client', schema); params.set('client', scheme);
} }
// TODO: Android app scheme not implemented // TODO: Android app scheme not implemented
@@ -87,7 +87,7 @@ function OAuthProvider({
BUILD_CONFIG.serverUrlPrefix + `/oauth/login?${params.toString()}`; BUILD_CONFIG.serverUrlPrefix + `/oauth/login?${params.toString()}`;
popupWindow(oauthUrl); popupWindow(oauthUrl);
}, [popupWindow, provider, redirectUrl, schema]); }, [popupWindow, provider, redirectUrl, scheme]);
return ( return (
<Button <Button
@@ -5,6 +5,10 @@ import {
SettingWrapper, SettingWrapper,
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import { useAppUpdater } from '@affine/core/components/hooks/use-app-updater'; import { useAppUpdater } from '@affine/core/components/hooks/use-app-updater';
import {
appIconMap,
appNames,
} from '@affine/core/modules/open-in-app/constant';
import { UrlService } from '@affine/core/modules/url'; import { UrlService } from '@affine/core/modules/url';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { mixpanel } from '@affine/track'; import { mixpanel } from '@affine/track';
@@ -13,7 +17,6 @@ import { useService } from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper';
import { appIconMap, appNames } from '../../../../../desktop/pages/open-app';
import { relatedLinks } from './config'; import { relatedLinks } from './config';
import * as styles from './style.css'; import * as styles from './style.css';
import { UpdateCheckSection } from './update-check-section'; import { UpdateCheckSection } from './update-check-section';
@@ -20,8 +20,8 @@ const UpgradeSuccessLayout = ({
const { jumpToIndex, openInApp } = useNavigateHelper(); const { jumpToIndex, openInApp } = useNavigateHelper();
const openAffine = useCallback(() => { const openAffine = useCallback(() => {
if (params.get('schema')) { if (params.get('scheme')) {
openInApp(params.get('schema') ?? 'affine', 'bring-to-front'); openInApp(params.get('scheme') ?? 'affine', 'bring-to-front');
} else { } else {
jumpToIndex(); jumpToIndex();
} }
@@ -19,6 +19,7 @@ import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive'; import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
import { DocInfoService } from '@affine/core/modules/doc-info'; import { DocInfoService } from '@affine/core/modules/doc-info';
import { EditorService } from '@affine/core/modules/editor'; import { EditorService } from '@affine/core/modules/editor';
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app/utils';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { ViewService } from '@affine/core/modules/workbench/services/view'; import { ViewService } from '@affine/core/modules/workbench/services/view';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
@@ -33,6 +34,7 @@ import {
HistoryIcon, HistoryIcon,
ImportIcon, ImportIcon,
InformationIcon, InformationIcon,
LocalWorkspaceIcon,
OpenInNewIcon, OpenInNewIcon,
PageIcon, PageIcon,
SaveIcon, SaveIcon,
@@ -258,6 +260,13 @@ export const PageHeaderMenuButton = ({
</> </>
); );
const onOpenInDesktop = useCallback(() => {
const url = getOpenUrlInDesktopAppLink(window.location.href, true);
if (url) {
window.open(url, '_blank');
}
}, []);
const EditMenu = ( const EditMenu = (
<> <>
{showResponsiveMenu ? ResponsiveMenuItems : null} {showResponsiveMenu ? ResponsiveMenuItems : null}
@@ -362,6 +371,15 @@ export const PageHeaderMenuButton = ({
data-testid="editor-option-menu-delete" data-testid="editor-option-menu-delete"
onSelect={handleOpenTrashModal} onSelect={handleOpenTrashModal}
/> />
{BUILD_CONFIG.isWeb ? (
<MenuItem
prefixIcon={<LocalWorkspaceIcon />}
data-testid="editor-option-menu-link"
onSelect={onOpenInDesktop}
>
{t['com.affine.header.option.open-in-desktop']()}
</MenuItem>
) : null}
</> </>
); );
if (isInTrash) { if (isInTrash) {
@@ -160,9 +160,9 @@ export function useNavigateHelper() {
); );
const openInApp = useCallback( const openInApp = useCallback(
(schema: string, path: string) => { (scheme: string, path: string) => {
const encodedUrl = encodeURIComponent(`${schema}://${path}`); const encodedUrl = encodeURIComponent(`${scheme}://${path}`);
return navigate(`/open-app/url?schema=${schema}&url=${encodedUrl}`); return navigate(`/open-app/url?scheme=${scheme}&url=${encodedUrl}`);
}, },
[navigate] [navigate]
); );
@@ -1,52 +1,22 @@
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import {
appIconMap,
appNames,
appSchemes,
type Channel,
schemeToChannel,
} from '@affine/core/modules/open-in-app/constant';
import type { GetCurrentUserQuery } from '@affine/graphql'; import type { GetCurrentUserQuery } from '@affine/graphql';
import { fetcher, getCurrentUserQuery } from '@affine/graphql'; import { fetcher, getCurrentUserQuery } from '@affine/graphql';
import { Trans, useI18n } from '@affine/i18n'; import { Trans, useI18n } from '@affine/i18n';
import { Logo1Icon } from '@blocksuite/icons/rc'; import { Logo1Icon } from '@blocksuite/icons/rc';
import { useCallback, useMemo } from 'react'; import { useCallback } from 'react';
import type { LoaderFunction } from 'react-router-dom'; import type { LoaderFunction } from 'react-router-dom';
import { useLoaderData, useSearchParams } from 'react-router-dom'; import { useLoaderData, useSearchParams } from 'react-router-dom';
import { z } from 'zod';
import * as styles from './open-app.css'; import * as styles from './open-app.css';
let lastOpened = ''; let lastOpened = '';
const appSchemas = z.enum([
'affine',
'affine-canary',
'affine-beta',
'affine-internal',
'affine-dev',
]);
const appChannelSchema = z.enum(['stable', 'canary', 'beta', 'internal']);
type Schema = z.infer<typeof appSchemas>;
type Channel = z.infer<typeof appChannelSchema>;
const schemaToChanel = {
affine: 'stable',
'affine-canary': 'canary',
'affine-beta': 'beta',
'affine-internal': 'internal',
'affine-dev': 'canary', // dev does not have a dedicated app. use canary as the placeholder.
} as Record<Schema, Channel>;
export const appIconMap = {
stable: '/imgs/app-icon-stable.ico',
canary: '/imgs/app-icon-canary.ico',
beta: '/imgs/app-icon-beta.ico',
internal: '/imgs/app-icon-internal.ico',
} satisfies Record<Channel, string>;
export const appNames = {
stable: 'AFFiNE',
canary: 'AFFiNE Canary',
beta: 'AFFiNE Beta',
internal: 'AFFiNE Internal',
} satisfies Record<Channel, string>;
interface OpenAppProps { interface OpenAppProps {
urlToOpen?: string | null; urlToOpen?: string | null;
channel: Channel; channel: Channel;
@@ -65,10 +35,8 @@ const OpenAppImpl = ({ urlToOpen, channel }: OpenAppProps) => {
}, [channel]); }, [channel]);
const appIcon = appIconMap[channel]; const appIcon = appIconMap[channel];
const appName = appNames[channel]; const appName = appNames[channel];
const [params] = useSearchParams();
const autoOpen = useMemo(() => params.get('open') !== 'false', [params]);
if (urlToOpen && lastOpened !== urlToOpen && autoOpen) { if (urlToOpen && lastOpened !== urlToOpen) {
lastOpened = urlToOpen; lastOpened = urlToOpen;
location.href = urlToOpen; location.href = urlToOpen;
} }
@@ -152,9 +120,9 @@ const OpenUrl = () => {
params.delete('url'); params.delete('url');
const urlObj = new URL(urlToOpen || ''); const urlObj = new URL(urlToOpen || '');
const maybeSchema = appSchemas.safeParse(urlObj.protocol.replace(':', '')); const maybeScheme = appSchemes.safeParse(urlObj.protocol.replace(':', ''));
const channel = const channel =
schemaToChanel[maybeSchema.success ? maybeSchema.data : 'affine']; schemeToChannel[maybeScheme.success ? maybeScheme.data : 'affine'];
params.forEach((v, k) => { params.forEach((v, k) => {
urlObj.searchParams.set(k, v); urlObj.searchParams.set(k, v);
@@ -170,16 +138,16 @@ const OpenOAuthJwt = () => {
const { currentUser } = useLoaderData() as LoaderData; const { currentUser } = useLoaderData() as LoaderData;
const [params] = useSearchParams(); const [params] = useSearchParams();
const maybeSchema = appSchemas.safeParse(params.get('schema')); const maybeScheme = appSchemes.safeParse(params.get('scheme'));
const schema = maybeSchema.success ? maybeSchema.data : 'affine'; const scheme = maybeScheme.success ? maybeScheme.data : 'affine';
const next = params.get('next'); const next = params.get('next');
const channel = schemaToChanel[schema as Schema]; const channel = schemeToChannel[scheme];
if (!currentUser || !currentUser?.token?.sessionToken) { if (!currentUser || !currentUser?.token?.sessionToken) {
return null; return null;
} }
const urlToOpen = `${schema}://signin-redirect?token=${ const urlToOpen = `${scheme}://signin-redirect?token=${
currentUser.token.sessionToken currentUser.token.sessionToken
}&next=${next || ''}`; }&next=${next || ''}`;
@@ -199,12 +167,19 @@ export const Component = () => {
export const loader: LoaderFunction = async args => { export const loader: LoaderFunction = async args => {
const action = args.params.action || ''; const action = args.params.action || '';
const res = await fetcher({
query: getCurrentUserQuery,
}).catch(console.error);
return { if (action === 'signin-redirect') {
action, const res = await fetcher({
currentUser: res?.currentUser || null, query: getCurrentUserQuery,
}; }).catch(console.error);
return {
action,
currentUser: res?.currentUser || null,
};
} else {
return {
action,
};
}
}; };
@@ -119,7 +119,7 @@ export class AuthService extends Service {
) { ) {
track.$.$.auth.signIn({ method: 'magic-link' }); track.$.$.auth.signIn({ method: 'magic-link' });
try { try {
const scheme = this.urlService.getClientSchema(); const scheme = this.urlService.getClientScheme();
const magicLinkUrlParams = new URLSearchParams(); const magicLinkUrlParams = new URLSearchParams();
if (redirectUrl) { if (redirectUrl) {
magicLinkUrlParams.set('redirect_uri', redirectUrl); magicLinkUrlParams.set('redirect_uri', redirectUrl);
@@ -23,14 +23,14 @@ const SUBSCRIPTION_CACHE_KEY = 'subscription:';
const getDefaultSubscriptionSuccessCallbackLink = ( const getDefaultSubscriptionSuccessCallbackLink = (
plan: SubscriptionPlan | null, plan: SubscriptionPlan | null,
schema?: string scheme?: string
) => { ) => {
const path = const path =
plan === SubscriptionPlan.AI ? '/ai-upgrade-success' : '/upgrade-success'; plan === SubscriptionPlan.AI ? '/ai-upgrade-success' : '/upgrade-success';
const urlString = getAffineCloudBaseUrl() + path; const urlString = getAffineCloudBaseUrl() + path;
const url = new URL(urlString); const url = new URL(urlString);
if (schema) { if (scheme) {
url.searchParams.set('schema', schema); url.searchParams.set('scheme', scheme);
} }
return url.toString(); return url.toString();
}; };
@@ -133,7 +133,7 @@ export class SubscriptionStore extends Store {
input.successCallbackLink || input.successCallbackLink ||
getDefaultSubscriptionSuccessCallbackLink( getDefaultSubscriptionSuccessCallbackLink(
input.plan, input.plan,
this.urlService.getClientSchema() this.urlService.getClientScheme()
), ),
}, },
}, },
@@ -0,0 +1,44 @@
import { z } from 'zod';
export const appSchemes = z.enum([
'affine',
'affine-canary',
'affine-beta',
'affine-internal',
'affine-dev',
]);
const appChannelSchemes = z.enum(['stable', 'canary', 'beta', 'internal']);
export type Scheme = z.infer<typeof appSchemes>;
export type Channel = z.infer<typeof appChannelSchemes>;
export const schemeToChannel = {
affine: 'stable',
'affine-canary': 'canary',
'affine-beta': 'beta',
'affine-internal': 'internal',
'affine-dev': 'canary', // dev does not have a dedicated app. use canary as the placeholder.
} as Record<Scheme, Channel>;
export const channelToScheme = {
stable: 'affine',
canary:
process.env.NODE_ENV === 'development' ? 'affine-dev' : 'affine-canary',
beta: 'affine-beta',
internal: 'affine-internal',
} as Record<Channel, Scheme>;
export const appIconMap = {
stable: '/imgs/app-icon-stable.ico',
canary: '/imgs/app-icon-canary.ico',
beta: '/imgs/app-icon-beta.ico',
internal: '/imgs/app-icon-internal.ico',
} satisfies Record<Channel, string>;
export const appNames = {
stable: 'AFFiNE',
canary: 'AFFiNE Canary',
beta: 'AFFiNE Beta',
internal: 'AFFiNE Internal',
} satisfies Record<Channel, string>;
@@ -0,0 +1,32 @@
import { DebugLogger } from '@affine/debug';
import { channelToScheme } from './constant';
const logger = new DebugLogger('open-in-app');
// return an AFFiNE app's url to be opened in desktop app
export const getOpenUrlInDesktopAppLink = (
url: string,
newTab = false,
scheme = channelToScheme[BUILD_CONFIG.appBuildType]
) => {
if (!scheme) {
return null;
}
const urlObject = new URL(url);
const params = urlObject.searchParams;
if (newTab) {
params.set('new-tab', '1');
}
try {
return new URL(
`${scheme}://${urlObject.host}${urlObject.pathname}?${params.toString()}#${urlObject.hash}`
).toString();
} catch (e) {
logger.error('Failed to get open url in desktop app link', e);
return null;
}
};
@@ -1,10 +1,10 @@
import type { Framework } from '@toeverything/infra'; import type { Framework } from '@toeverything/infra';
import { ClientSchemaProvider } from './providers/client-schema'; import { ClientSchemeProvider } from './providers/client-schema';
import { PopupWindowProvider } from './providers/popup-window'; import { PopupWindowProvider } from './providers/popup-window';
import { UrlService } from './services/url'; import { UrlService } from './services/url';
export { ClientSchemaProvider } from './providers/client-schema'; export { ClientSchemeProvider } from './providers/client-schema';
export { PopupWindowProvider } from './providers/popup-window'; export { PopupWindowProvider } from './providers/popup-window';
export { UrlService } from './services/url'; export { UrlService } from './services/url';
@@ -14,7 +14,7 @@ export const configureUrlModule = (container: Framework) => {
f => f =>
new UrlService( new UrlService(
f.getOptional(PopupWindowProvider), f.getOptional(PopupWindowProvider),
f.getOptional(ClientSchemaProvider) f.getOptional(ClientSchemeProvider)
) )
); );
}; };
@@ -1,12 +1,12 @@
import { createIdentifier } from '@toeverything/infra'; import { createIdentifier } from '@toeverything/infra';
export interface ClientSchemaProvider { export interface ClientSchemeProvider {
/** /**
* Get the client schema in the current environment, used for the user to complete the authentication process in the browser and redirect back to the app. * Get the client schema in the current environment, used for the user to complete the authentication process in the browser and redirect back to the app.
*/ */
getClientSchema(): string | undefined; getClientScheme(): string | undefined;
} }
export const ClientSchemaProvider = createIdentifier<ClientSchemaProvider>( export const ClientSchemeProvider = createIdentifier<ClientSchemeProvider>(
'ClientSchemaProvider' 'ClientSchemeProvider'
); );
@@ -1,19 +1,19 @@
import { Service } from '@toeverything/infra'; import { Service } from '@toeverything/infra';
import type { ClientSchemaProvider } from '../providers/client-schema'; import type { ClientSchemeProvider } from '../providers/client-schema';
import type { PopupWindowProvider } from '../providers/popup-window'; import type { PopupWindowProvider } from '../providers/popup-window';
export class UrlService extends Service { export class UrlService extends Service {
constructor( constructor(
// those providers are optional, because they are not always available in some environments // those providers are optional, because they are not always available in some environments
private readonly popupWindowProvider?: PopupWindowProvider, private readonly popupWindowProvider?: PopupWindowProvider,
private readonly clientSchemaProvider?: ClientSchemaProvider private readonly clientSchemeProvider?: ClientSchemeProvider
) { ) {
super(); super();
} }
getClientSchema() { getClientScheme() {
return this.clientSchemaProvider?.getClientSchema(); return this.clientSchemeProvider?.getClientScheme();
} }
/** /**
@@ -1,3 +1,4 @@
import { DebugLogger } from '@affine/debug';
import { apis } from '@affine/electron-api'; import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { DocCollection } from '@blocksuite/affine/store'; import { DocCollection } from '@blocksuite/affine/store';
@@ -27,6 +28,32 @@ export const LOCAL_WORKSPACE_LOCAL_STORAGE_KEY = 'affine-local-workspace';
const LOCAL_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY = const LOCAL_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY =
'affine-local-workspace-changed'; 'affine-local-workspace-changed';
const logger = new DebugLogger('local-workspace');
export function getLocalWorkspaceIds(): string[] {
try {
return JSON.parse(
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
);
} catch (e) {
logger.error('Failed to get local workspace ids', e);
return [];
}
}
export function setLocalWorkspaceIds(
idsOrUpdater: string[] | ((ids: string[]) => string[])
) {
localStorage.setItem(
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
JSON.stringify(
typeof idsOrUpdater === 'function'
? idsOrUpdater(getLocalWorkspaceIds())
: idsOrUpdater
)
);
}
export class LocalWorkspaceFlavourProvider export class LocalWorkspaceFlavourProvider
extends Service extends Service
implements WorkspaceFlavourProvider implements WorkspaceFlavourProvider
@@ -43,13 +70,7 @@ export class LocalWorkspaceFlavourProvider
); );
async deleteWorkspace(id: string): Promise<void> { async deleteWorkspace(id: string): Promise<void> {
const allWorkspaceIDs: string[] = JSON.parse( setLocalWorkspaceIds(ids => ids.filter(x => x !== id));
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
);
localStorage.setItem(
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
JSON.stringify(allWorkspaceIDs.filter(x => x !== id))
);
if (BUILD_CONFIG.isElectron && apis) { if (BUILD_CONFIG.isElectron && apis) {
await apis.workspace.delete(id); await apis.workspace.delete(id);
@@ -88,14 +109,7 @@ export class LocalWorkspaceFlavourProvider
} }
// save workspace id to local storage // save workspace id to local storage
const allWorkspaceIDs: string[] = JSON.parse( setLocalWorkspaceIds(ids => [...ids, id]);
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
);
allWorkspaceIDs.push(id);
localStorage.setItem(
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
JSON.stringify(allWorkspaceIDs)
);
// notify all browser tabs, so they can update their workspace list // notify all browser tabs, so they can update their workspace list
this.notifyChannel.postMessage(id); this.notifyChannel.postMessage(id);
@@ -106,9 +120,10 @@ export class LocalWorkspaceFlavourProvider
new Observable<WorkspaceMetadata[]>(subscriber => { new Observable<WorkspaceMetadata[]>(subscriber => {
let last: WorkspaceMetadata[] | null = null; let last: WorkspaceMetadata[] | null = null;
const emit = () => { const emit = () => {
const value = JSON.parse( const value = getLocalWorkspaceIds().map(id => ({
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]' id,
).map((id: string) => ({ id, flavour: WorkspaceFlavour.LOCAL })); flavour: WorkspaceFlavour.LOCAL,
}));
if (isEqual(last, value)) return; if (isEqual(last, value)) return;
subscriber.next(value); subscriber.next(value);
last = value; last = value;
@@ -1,5 +1,5 @@
{ {
"ar": 85, "ar": 84,
"ca": 6, "ca": 6,
"da": 6, "da": 6,
"de": 31, "de": 31,
@@ -14,7 +14,7 @@
"ko": 89, "ko": 89,
"pl": 0, "pl": 0,
"pt-BR": 97, "pt-BR": 97,
"ru": 83, "ru": 82,
"sv-SE": 5, "sv-SE": 5,
"ur": 3, "ur": 3,
"zh-Hans": 99, "zh-Hans": 99,
@@ -478,6 +478,7 @@
"com.affine.filterList.button.add": "Add filter", "com.affine.filterList.button.add": "Add filter",
"com.affine.header.option.add-tag": "Add tag", "com.affine.header.option.add-tag": "Add tag",
"com.affine.header.option.duplicate": "Duplicate", "com.affine.header.option.duplicate": "Duplicate",
"com.affine.header.option.open-in-desktop": "Open in desktop app",
"com.affine.header.option.view-frame": "View all frames", "com.affine.header.option.view-frame": "View all frames",
"com.affine.header.option.view-toc": "View table of contents", "com.affine.header.option.view-toc": "View table of contents",
"com.affine.helpIsland.contactUs": "Contact us", "com.affine.helpIsland.contactUs": "Contact us",