mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user