feat(core): open app in electron app entry (#8637)

fix PD-208
fix PD-210
fix PD-209
fix AF-1495
This commit is contained in:
pengx17
2024-10-31 06:16:32 +00:00
parent 5d92c900d1
commit 0f8b273134
36 changed files with 887 additions and 226 deletions

View File

@@ -33,7 +33,12 @@ export const topNavLink = style({
textDecoration: 'none',
padding: '4px 18px',
});
export const tryAgainLink = style({
export const promptLinks = style({
display: 'flex',
columnGap: 16,
});
export const promptLink = style({
color: cssVar('linkColor'),
fontWeight: 500,
textDecoration: 'none',
@@ -49,3 +54,11 @@ export const prompt = style({
marginTop: 20,
marginBottom: 12,
});
export const editSettingsLink = style({
fontWeight: 500,
textDecoration: 'none',
color: cssVar('linkColor'),
fontSize: cssVar('fontSm'),
position: 'absolute',
bottom: 48,
});

View File

@@ -1,134 +1,32 @@
import { Button } from '@affine/component/ui/button';
import {
appIconMap,
appNames,
appSchemes,
type Channel,
schemeToChannel,
} from '@affine/core/modules/open-in-app/constant';
import { OpenInAppPage } from '@affine/core/modules/open-in-app/views/open-in-app-page';
import { appSchemes } from '@affine/core/utils';
import type { GetCurrentUserQuery } from '@affine/graphql';
import { fetcher, getCurrentUserQuery } from '@affine/graphql';
import { Trans, useI18n } from '@affine/i18n';
import { Logo1Icon } from '@blocksuite/icons/rc';
import { useCallback } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { useLoaderData, useSearchParams } from 'react-router-dom';
import * as styles from './open-app.css';
let lastOpened = '';
interface OpenAppProps {
urlToOpen?: string | null;
channel: Channel;
}
interface LoaderData {
action: 'url' | 'signin-redirect';
currentUser?: GetCurrentUserQuery['currentUser'];
}
const OpenAppImpl = ({ urlToOpen, channel }: OpenAppProps) => {
const t = useI18n();
const openDownloadLink = useCallback(() => {
const url = `https://affine.pro/download?channel=${channel}`;
open(url, '_blank');
}, [channel]);
const appIcon = appIconMap[channel];
const appName = appNames[channel];
if (urlToOpen && lastOpened !== urlToOpen) {
lastOpened = urlToOpen;
location.href = urlToOpen;
}
const OpenUrl = () => {
const [params] = useSearchParams();
const urlToOpen = params.get('url');
if (!urlToOpen) {
return null;
}
return (
<div className={styles.root}>
<div className={styles.topNav}>
<a href="/" rel="noreferrer" className={styles.affineLogo}>
<Logo1Icon width={24} height={24} />
</a>
<div className={styles.topNavLinks}>
<a
href="https://affine.pro"
target="_blank"
rel="noreferrer"
className={styles.topNavLink}
>
Official Website
</a>
<a
href="https://community.affine.pro/home"
target="_blank"
rel="noreferrer"
className={styles.topNavLink}
>
AFFiNE Community
</a>
<a
href="https://affine.pro/blog"
target="_blank"
rel="noreferrer"
className={styles.topNavLink}
>
Blog
</a>
<a
href="https://affine.pro/about-us"
target="_blank"
rel="noreferrer"
className={styles.topNavLink}
>
Contact us
</a>
</div>
<Button onClick={openDownloadLink}>
{t['com.affine.auth.open.affine.download-app']()}
</Button>
</div>
<div className={styles.centerContent}>
<img src={appIcon} alt={appName} width={120} height={120} />
<div className={styles.prompt}>
<Trans i18nKey="com.affine.auth.open.affine.prompt">
Open {appName} app now
</Trans>
</div>
<a
className={styles.tryAgainLink}
href={urlToOpen}
target="_blank"
rel="noreferrer"
>
{t['com.affine.auth.open.affine.try-again']()}
</a>
</div>
</div>
);
};
const OpenUrl = () => {
const [params] = useSearchParams();
const urlToOpen = params.get('url');
params.delete('url');
const urlObj = new URL(urlToOpen || '');
const maybeScheme = appSchemes.safeParse(urlObj.protocol.replace(':', ''));
const channel =
schemeToChannel[maybeScheme.success ? maybeScheme.data : 'affine'];
params.forEach((v, k) => {
urlObj.searchParams.set(k, v);
});
return <OpenAppImpl urlToOpen={urlObj.toString()} channel={channel} />;
return <OpenInAppPage urlToOpen={urlObj.toString()} />;
};
/**
@@ -141,7 +39,6 @@ const OpenOAuthJwt = () => {
const maybeScheme = appSchemes.safeParse(params.get('scheme'));
const scheme = maybeScheme.success ? maybeScheme.data : 'affine';
const next = params.get('next');
const channel = schemeToChannel[scheme];
if (!currentUser || !currentUser?.token?.sessionToken) {
return null;
@@ -151,7 +48,7 @@ const OpenOAuthJwt = () => {
currentUser.token.sessionToken
}&next=${next || ''}`;
return <OpenAppImpl urlToOpen={urlToOpen} channel={channel} />;
return <OpenInAppPage urlToOpen={urlToOpen} />;
};
export const Component = () => {

View File

@@ -97,10 +97,7 @@ export const Component = (): ReactElement => {
}, [listLoading, meta, workspaceNotFound, workspacesService]);
if (workspaceNotFound) {
if (
!BUILD_CONFIG.isElectron /* only browser has share page */ &&
detailDocRoute
) {
if (detailDocRoute) {
return (
<SharePage
docId={detailDocRoute.docId}

View File

@@ -6,6 +6,7 @@ export const root = style({
height: '100%',
overflow: 'hidden',
width: '100%',
flexDirection: 'column',
});
export const mainContainer = style({
@@ -56,3 +57,9 @@ export const linkText = style({
fontWeight: 700,
whiteSpace: 'nowrap',
});
export const shareCard = style({
position: 'fixed',
bottom: '16px',
left: '16px',
});

View File

@@ -7,6 +7,8 @@ import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-he
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
import { SharePageNotFoundError } from '@affine/core/components/share-page-not-found-error';
import { AppContainer, MainContainer } from '@affine/core/components/workspace';
import { OpenInAppCard } from '@affine/core/modules/app-sidebar/views';
import { AppTabsHeader } from '@affine/core/modules/app-tabs-header';
import { AuthService, FetchService } from '@affine/core/modules/cloud';
import {
type Editor,
@@ -16,6 +18,8 @@ import {
} from '@affine/core/modules/editor';
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
import { ShareReaderService } from '@affine/core/modules/share-doc';
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
import { DesktopStateSynchronizer } from '@affine/core/modules/workbench/services/desktop-state-synchronizer';
import { CloudBlobStorage } from '@affine/core/modules/workspace-engine';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
@@ -35,11 +39,18 @@ import {
ReadonlyDocStorage,
useLiveData,
useService,
useServiceOptional,
useServices,
WorkspacesService,
} from '@toeverything/infra';
import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
type PropsWithChildren,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { PageNotFound } from '../../404';
@@ -125,6 +136,75 @@ export const SharePage = ({
}
};
interface SharePageContainerProps {
pageId: string;
pageTitle?: string;
publishMode: DocMode;
isTemplate?: boolean;
templateName?: string;
templateSnapshotUrl?: string;
}
const SharePageWebContainer = ({
children,
pageId,
publishMode,
isTemplate,
templateName,
templateSnapshotUrl,
}: PropsWithChildren<SharePageContainerProps>) => {
return (
<MainContainer>
<div className={styles.root}>
<div className={styles.mainContainer}>
<ShareHeader
pageId={pageId}
publishMode={publishMode}
isTemplate={isTemplate}
templateName={templateName}
snapshotUrl={templateSnapshotUrl}
/>
{children}
<SharePageFooter />
</div>
<OpenInAppCard className={styles.shareCard} />
</div>
</MainContainer>
);
};
const SharePageDesktopContainer = ({
children,
pageId,
pageTitle,
publishMode,
isTemplate,
templateName,
templateSnapshotUrl,
}: PropsWithChildren<SharePageContainerProps>) => {
useServiceOptional(DesktopStateSynchronizer);
return (
<MainContainer>
{/* share page does not have ViewRoot so the following does not work yet */}
<ViewTitle title={pageTitle || ''} />
<ViewIcon icon="doc" />
<div className={styles.root}>
<AppTabsHeader />
<div className={styles.mainContainer}>
<ShareHeader
pageId={pageId}
publishMode={publishMode}
isTemplate={isTemplate}
templateName={templateName}
snapshotUrl={templateSnapshotUrl}
/>
{children}
</div>
</div>
</MainContainer>
);
};
const SharePageInner = ({
workspaceId,
docId,
@@ -274,41 +354,40 @@ const SharePageInner = ({
return;
}
const Container = BUILD_CONFIG.isElectron
? SharePageDesktopContainer
: SharePageWebContainer;
return (
<FrameworkScope scope={workspace.scope}>
<FrameworkScope scope={page.scope}>
<FrameworkScope scope={editor.scope}>
<AppContainer>
<MainContainer>
<div className={styles.root}>
<div className={styles.mainContainer}>
<ShareHeader
pageId={page.id}
publishMode={publishMode}
isTemplate={isTemplate}
templateName={templateName}
snapshotUrl={templateSnapshotUrl}
/>
<Scrollable.Root>
<Scrollable.Viewport
className={clsx(
'affine-page-viewport',
styles.editorContainer
)}
>
<PageDetailEditor onLoad={onEditorLoad} />
{publishMode === 'page' ? <ShareFooter /> : null}
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
<EditorOutlineViewer
editor={editorContainer}
show={publishMode === 'page'}
/>
<SharePageFooter />
</div>
</div>
</MainContainer>
<Container
pageTitle={pageTitle}
pageId={page.id}
publishMode={publishMode}
isTemplate={isTemplate}
templateName={templateName}
templateSnapshotUrl={templateSnapshotUrl}
>
<Scrollable.Root>
<Scrollable.Viewport
className={clsx(
'affine-page-viewport',
styles.editorContainer
)}
>
<PageDetailEditor onLoad={onEditorLoad} />
{publishMode === 'page' ? <ShareFooter /> : null}
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
<EditorOutlineViewer
editor={editorContainer}
show={publishMode === 'page'}
/>
</Container>
<PeekViewManagerModal />
</AppContainer>
</FrameworkScope>