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
@@ -119,7 +119,7 @@ export class AuthService extends Service {
) {
track.$.$.auth.signIn({ method: 'magic-link' });
try {
const scheme = this.urlService.getClientSchema();
const scheme = this.urlService.getClientScheme();
const magicLinkUrlParams = new URLSearchParams();
if (redirectUrl) {
magicLinkUrlParams.set('redirect_uri', redirectUrl);
@@ -23,14 +23,14 @@ const SUBSCRIPTION_CACHE_KEY = 'subscription:';
const getDefaultSubscriptionSuccessCallbackLink = (
plan: SubscriptionPlan | null,
schema?: string
scheme?: string
) => {
const path =
plan === SubscriptionPlan.AI ? '/ai-upgrade-success' : '/upgrade-success';
const urlString = getAffineCloudBaseUrl() + path;
const url = new URL(urlString);
if (schema) {
url.searchParams.set('schema', schema);
if (scheme) {
url.searchParams.set('scheme', scheme);
}
return url.toString();
};
@@ -133,7 +133,7 @@ export class SubscriptionStore extends Store {
input.successCallbackLink ||
getDefaultSubscriptionSuccessCallbackLink(
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 { ClientSchemaProvider } from './providers/client-schema';
import { ClientSchemeProvider } from './providers/client-schema';
import { PopupWindowProvider } from './providers/popup-window';
import { UrlService } from './services/url';
export { ClientSchemaProvider } from './providers/client-schema';
export { ClientSchemeProvider } from './providers/client-schema';
export { PopupWindowProvider } from './providers/popup-window';
export { UrlService } from './services/url';
@@ -14,7 +14,7 @@ export const configureUrlModule = (container: Framework) => {
f =>
new UrlService(
f.getOptional(PopupWindowProvider),
f.getOptional(ClientSchemaProvider)
f.getOptional(ClientSchemeProvider)
)
);
};
@@ -1,12 +1,12 @@
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.
*/
getClientSchema(): string | undefined;
getClientScheme(): string | undefined;
}
export const ClientSchemaProvider = createIdentifier<ClientSchemaProvider>(
'ClientSchemaProvider'
export const ClientSchemeProvider = createIdentifier<ClientSchemeProvider>(
'ClientSchemeProvider'
);
@@ -1,19 +1,19 @@
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';
export class UrlService extends Service {
constructor(
// those providers are optional, because they are not always available in some environments
private readonly popupWindowProvider?: PopupWindowProvider,
private readonly clientSchemaProvider?: ClientSchemaProvider
private readonly clientSchemeProvider?: ClientSchemeProvider
) {
super();
}
getClientSchema() {
return this.clientSchemaProvider?.getClientSchema();
getClientScheme() {
return this.clientSchemeProvider?.getClientScheme();
}
/**
@@ -1,3 +1,4 @@
import { DebugLogger } from '@affine/debug';
import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace';
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 =
'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
extends Service
implements WorkspaceFlavourProvider
@@ -43,13 +70,7 @@ export class LocalWorkspaceFlavourProvider
);
async deleteWorkspace(id: string): Promise<void> {
const allWorkspaceIDs: string[] = JSON.parse(
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
);
localStorage.setItem(
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
JSON.stringify(allWorkspaceIDs.filter(x => x !== id))
);
setLocalWorkspaceIds(ids => ids.filter(x => x !== id));
if (BUILD_CONFIG.isElectron && apis) {
await apis.workspace.delete(id);
@@ -88,14 +109,7 @@ export class LocalWorkspaceFlavourProvider
}
// save workspace id to local storage
const allWorkspaceIDs: string[] = JSON.parse(
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
);
allWorkspaceIDs.push(id);
localStorage.setItem(
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
JSON.stringify(allWorkspaceIDs)
);
setLocalWorkspaceIds(ids => [...ids, id]);
// notify all browser tabs, so they can update their workspace list
this.notifyChannel.postMessage(id);
@@ -106,9 +120,10 @@ export class LocalWorkspaceFlavourProvider
new Observable<WorkspaceMetadata[]>(subscriber => {
let last: WorkspaceMetadata[] | null = null;
const emit = () => {
const value = JSON.parse(
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
).map((id: string) => ({ id, flavour: WorkspaceFlavour.LOCAL }));
const value = getLocalWorkspaceIds().map(id => ({
id,
flavour: WorkspaceFlavour.LOCAL,
}));
if (isEqual(last, value)) return;
subscriber.next(value);
last = value;