diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.css.ts b/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.css.ts index ee56663922..1df2545fed 100644 --- a/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.css.ts +++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.css.ts @@ -43,3 +43,8 @@ export const hiddenInput = style({ height: '0', position: 'absolute', }); + +export const timeRow = style({ + marginTop: 20, + borderBottom: 4, +}); diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.tsx b/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.tsx index 9e4ea911c4..c2ac243957 100644 --- a/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.tsx @@ -78,7 +78,7 @@ export const InfoModal = ({ ); }; -const InfoTable = ({ +export const InfoTable = ({ onClose, docId, readonly, @@ -106,8 +106,8 @@ const InfoTable = ({ ); return ( -
- +
+ {backlinks && backlinks.length > 0 ? ( <> diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/links-row.tsx b/packages/frontend/core/src/components/affine/page-properties/info-modal/links-row.tsx index 1d38911212..6e46f0e34f 100644 --- a/packages/frontend/core/src/components/affine/page-properties/info-modal/links-row.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/links-row.tsx @@ -8,15 +8,17 @@ import * as styles from './links-row.css'; export const LinksRow = ({ references, label, + className, onClick, }: { references: Backlink[] | Link[]; label: string; + className?: string; onClick?: () => void; }) => { const manager = useContext(managerContext); return ( -
+
{label} ยท {references.length}
diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/tags-row.css.ts b/packages/frontend/core/src/components/affine/page-properties/info-modal/tags-row.css.ts index 89a27aa750..29f50d305a 100644 --- a/packages/frontend/core/src/components/affine/page-properties/info-modal/tags-row.css.ts +++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/tags-row.css.ts @@ -1,5 +1,7 @@ import { cssVar } from '@toeverything/theme'; -import { style } from '@vanilla-extract/css'; +import { fallbackVar, style } from '@vanilla-extract/css'; + +import { rowHPadding } from '../styles.css'; export const icon = style({ fontSize: 16, @@ -65,7 +67,7 @@ export const rowValueCell = style({ ':hover': { backgroundColor: cssVar('hoverColor'), }, - padding: '6px 8px', + padding: `6px ${fallbackVar(rowHPadding, '8px')} 6px 8px`, border: `1px solid transparent`, color: cssVar('textPrimaryColor'), ':focus': { diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/tags-row.tsx b/packages/frontend/core/src/components/affine/page-properties/info-modal/tags-row.tsx index bd6da44eba..620bc1ab58 100644 --- a/packages/frontend/core/src/components/affine/page-properties/info-modal/tags-row.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/tags-row.tsx @@ -11,16 +11,21 @@ import * as styles from './tags-row.css'; export const TagsRow = ({ docId, readonly, + className, }: { docId: string; readonly: boolean; + className?: string; }) => { const t = useI18n(); const tagList = useService(TagService).tagList; const tagIds = useLiveData(tagList.tagIdsByPageId$(docId)); const empty = !tagIds || tagIds.length === 0; return ( -
+
diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.css.ts b/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.css.ts index a2c591691c..329588f103 100644 --- a/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.css.ts +++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.css.ts @@ -46,6 +46,4 @@ export const rowCell = style({ export const container = style({ display: 'flex', flexDirection: 'column', - marginTop: 20, - marginBottom: 4, }); diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.tsx b/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.tsx index c099d0ad28..88a1282182 100644 --- a/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/time-row.tsx @@ -1,6 +1,7 @@ import { i18nTime, useI18n } from '@affine/i18n'; import { DateTimeIcon, HistoryIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService, WorkspaceService } from '@toeverything/infra'; +import clsx from 'clsx'; import type { ConfigType } from 'dayjs'; import { useDebouncedValue } from 'foxact/use-debounced-value'; import { type ReactNode, useContext, useMemo } from 'react'; @@ -28,7 +29,13 @@ const RowComponent = ({ ); }; -export const TimeRow = ({ docId }: { docId: string }) => { +export const TimeRow = ({ + docId, + className, +}: { + docId: string; + className?: string; +}) => { const t = useI18n(); const manager = useContext(managerContext); const workspaceService = useService(WorkspaceService); @@ -88,5 +95,7 @@ export const TimeRow = ({ docId }: { docId: string }) => { const dTimestampElement = useDebouncedValue(timestampElement, 500); - return
{dTimestampElement}
; + return ( +
{dTimestampElement}
+ ); }; diff --git a/packages/frontend/core/src/components/affine/page-properties/styles.css.ts b/packages/frontend/core/src/components/affine/page-properties/styles.css.ts index fcccab3ebf..d34aed52aa 100644 --- a/packages/frontend/core/src/components/affine/page-properties/styles.css.ts +++ b/packages/frontend/core/src/components/affine/page-properties/styles.css.ts @@ -2,6 +2,8 @@ import { cssVar } from '@toeverything/theme'; import { createVar, globalStyle, style } from '@vanilla-extract/css'; const propertyNameCellWidth = createVar(); +export const rowHPadding = createVar(); +export const fontSize = createVar(); export const root = style({ display: 'flex', @@ -10,6 +12,16 @@ export const root = style({ fontFamily: cssVar('fontSansFamily'), vars: { [propertyNameCellWidth]: '160px', + [rowHPadding]: '6px', + [fontSize]: cssVar('fontSm'), + }, + '@container': { + [`viewport (width <= 640px)`]: { + vars: { + [rowHPadding]: '0px', + [fontSize]: cssVar('fontXs'), + }, + }, }, }); @@ -22,7 +34,7 @@ export const rootCentered = style({ padding: `0 ${cssVar('editorSidePadding', '24px')}`, '@container': { [`viewport (width <= 640px)`]: { - padding: '0 24px', + padding: '0 16px', }, }, }); @@ -39,7 +51,7 @@ export const tableHeaderInfoRow = style({ justifyContent: 'space-between', alignItems: 'center', color: cssVar('textSecondaryColor'), - fontSize: cssVar('fontSm'), + fontSize: fontSize, fontWeight: 500, minHeight: 34, '@media': { @@ -54,9 +66,9 @@ export const tableHeaderSecondaryRow = style({ flexDirection: 'row', alignItems: 'center', color: cssVar('textPrimaryColor'), - fontSize: cssVar('fontSm'), + fontSize: fontSize, fontWeight: 500, - padding: '0 6px', + padding: `0 ${rowHPadding}`, gap: '8px', height: 24, '@media': { @@ -82,7 +94,7 @@ export const spacer = style({ }); export const tableHeaderBacklinksHint = style({ - padding: '6px', + padding: `0 ${rowHPadding}`, cursor: 'pointer', borderRadius: '4px', ':hover': { @@ -103,7 +115,7 @@ export const tableHeaderTimestamp = style({ alignItems: 'start', gap: '8px', cursor: 'default', - padding: '0 6px', + padding: `0 ${rowHPadding}`, }); export const tableHeaderDivider = style({ @@ -273,7 +285,7 @@ export const editablePropertyRowCell = style([ export const propertyRowNameCell = style([ propertyRowCell, { - padding: 6, + padding: `6px ${rowHPadding}`, flexShrink: 0, color: cssVar('textSecondaryColor'), width: propertyNameCellWidth, @@ -316,7 +328,7 @@ export const propertyRowValueCell = style([ propertyRowCell, editablePropertyRowCell, { - padding: '6px 8px', + padding: `6px ${rowHPadding} 6px 6px`, border: `1px solid transparent`, color: cssVar('textPrimaryColor'), ':focus': { @@ -353,7 +365,7 @@ export const propertyRowValueTextarea = style([ propertyRowValueCell, { border: 'none', - padding: '6px 8px', + padding: `6px ${rowHPadding} 6px 8px`, height: '100%', position: 'absolute', top: 0, @@ -368,7 +380,7 @@ export const propertyRowValueTextareaInvisible = style([ propertyRowValueCell, { border: 'none', - padding: '6px 8px', + padding: `6px ${rowHPadding} 6px 8px`, visibility: 'hidden', whiteSpace: 'break-spaces', wordBreak: 'break-all', @@ -379,7 +391,7 @@ export const propertyRowValueTextareaInvisible = style([ export const propertyRowValueNumberCell = style([ propertyRowValueTextCell, { - padding: '6px 8px', + padding: `6px ${rowHPadding} 6px 8px`, }, ]); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx index 428fdcdbcc..076dbcdd3d 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx @@ -191,7 +191,7 @@ export const BlocksuiteDocEditor = forwardRef< ) : ( )} - + {!shared ? : null} { + const currentWorkspace = useService(WorkspaceService).workspace; + const docsService = useService(DocsService); + const docRecordList = docsService.list; + const docListReady = useLiveData(docRecordList.isReady$); + const docRecord = useLiveData(docRecordList.doc$(pageId)); + const viewService = useService(ViewService); + + const queryString = useLiveData( + viewService.view.queryString$<{ + mode?: string; + }>() + ); + + const queryStringMode = + queryString.mode && ['edgeless', 'page'].includes(queryString.mode) + ? (queryString.mode as DocMode) + : null; + + // We only read the querystring mode when entering, so use useState here. + const [initialQueryStringMode] = useState(() => queryStringMode); + + const [doc, setDoc] = useState(null); + const [editor, setEditor] = useState(null); + const editorMode = useLiveData(editor?.mode$); + + useLayoutEffect(() => { + if (!docRecord) { + return; + } + const { doc: opened, release } = docsService.open(pageId); + setDoc(opened); + return () => { + release(); + }; + }, [docRecord, docsService, pageId]); + + useLayoutEffect(() => { + if (!doc) { + return; + } + const editor = doc.scope + .get(EditorsService) + .createEditor(initialQueryStringMode || doc.getPrimaryMode() || 'page'); + setEditor(editor); + return () => { + editor.dispose(); + }; + }, [doc, initialQueryStringMode]); + + // update editor mode to queryString + useEffect(() => { + if (editorMode) { + viewService.view.updateQueryString( + { + mode: editorMode, + }, + { + replace: true, + } + ); + } + }, [editorMode, viewService.view]); + + // set sync engine priority target + useEffect(() => { + currentWorkspace.engine.doc.setPriority(pageId, 10); + return () => { + currentWorkspace.engine.doc.setPriority(pageId, 5); + }; + }, [currentWorkspace, pageId]); + + const isInTrash = useLiveData(doc?.meta$.map(meta => meta.trash)); + + useEffect(() => { + if (doc && isInTrash) { + currentWorkspace.docCollection.awarenessStore.setReadonly( + doc.blockSuiteDoc.blockCollection, + true + ); + } + }, [currentWorkspace.docCollection.awarenessStore, doc, isInTrash]); + + return { + doc, + editor, + docListReady, + }; +}; + +/** + * A common wrapper for detail page for both mobile and desktop page. + * It only contains the logic for page loading, context setup, but not the page content. + */ +export const DetailPageWrapper = ({ + pageId, + children, +}: PropsWithChildren<{ pageId: string }>) => { + const { doc, editor, docListReady } = useLoadDoc(pageId); + // if sync engine has been synced and the page is null, show 404 page. + if (docListReady && !doc) { + return ; + } + + if (!doc || !editor) { + return ; + } + + return ( + + {children} + + ); +}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index db02c609bd..a35326e778 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -1,16 +1,14 @@ import { Scrollable, useHasScrollTop } from '@affine/component'; -import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; import type { ChatPanel } from '@affine/core/blocksuite/presets/ai'; import { AIProvider } from '@affine/core/blocksuite/presets/ai'; import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding'; import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; -import type { Editor } from '@affine/core/modules/editor'; -import { EditorService, EditorsService } from '@affine/core/modules/editor'; +import { EditorService } from '@affine/core/modules/editor'; import { RecentDocsService } from '@affine/core/modules/quicksearch'; import { ViewService } from '@affine/core/modules/workbench/services/view'; -import type { DocMode, PageRootService } from '@blocksuite/blocks'; +import type { PageRootService } from '@blocksuite/blocks'; import { BookmarkBlockService, customImageProxyMiddleware, @@ -23,10 +21,8 @@ import { DisposableGroup } from '@blocksuite/global/utils'; import { AiIcon, FrameIcon, TocIcon, TodayIcon } from '@blocksuite/icons/rc'; import { type AffineEditorContainer } from '@blocksuite/presets'; import type { Doc as BlockSuiteDoc } from '@blocksuite/store'; -import type { Doc } from '@toeverything/infra'; import { DocService, - DocsService, FrameworkScope, globalBlockSuiteSchema, GlobalContextService, @@ -36,15 +32,7 @@ import { WorkspaceService, } from '@toeverything/infra'; import clsx from 'clsx'; -import type { ReactElement } from 'react'; -import { - memo, - useCallback, - useEffect, - useLayoutEffect, - useRef, - useState, -} from 'react'; +import { memo, useCallback, useEffect, useRef } from 'react'; import { useParams } from 'react-router-dom'; import type { Map as YMap } from 'yjs'; @@ -65,9 +53,9 @@ import { WorkbenchService, } from '../../../modules/workbench'; import { performanceRenderLogger } from '../../../shared'; -import { PageNotFound } from '../../404'; import * as styles from './detail-page.css'; import { DetailPageHeader } from './detail-page-header'; +import { DetailPageWrapper } from './detail-page-wrapper'; import { EditorChatPanel } from './tabs/chat'; import { EditorFramePanel } from './tabs/frame'; import { EditorJournalPanel } from './tabs/journal'; @@ -330,107 +318,6 @@ const DetailPageImpl = memo(function DetailPageImpl() { ); }); -export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => { - const currentWorkspace = useService(WorkspaceService).workspace; - const docsService = useService(DocsService); - const docRecordList = docsService.list; - const docListReady = useLiveData(docRecordList.isReady$); - const docRecord = useLiveData(docRecordList.doc$(pageId)); - const viewService = useService(ViewService); - - const queryString = useLiveData( - viewService.view.queryString$<{ - mode?: string; - }>() - ); - - const queryStringMode = - queryString.mode && ['edgeless', 'page'].includes(queryString.mode) - ? (queryString.mode as DocMode) - : null; - - // We only read the querystring mode when entering, so use useState here. - const [initialQueryStringMode] = useState(() => queryStringMode); - - const [doc, setDoc] = useState(null); - const [editor, setEditor] = useState(null); - const editorMode = useLiveData(editor?.mode$); - - useLayoutEffect(() => { - if (!docRecord) { - return; - } - const { doc: opened, release } = docsService.open(pageId); - setDoc(opened); - return () => { - release(); - }; - }, [docRecord, docsService, pageId]); - - useLayoutEffect(() => { - if (!doc) { - return; - } - const editor = doc.scope - .get(EditorsService) - .createEditor(initialQueryStringMode || doc.getPrimaryMode() || 'page'); - setEditor(editor); - return () => { - editor.dispose(); - }; - }, [doc, initialQueryStringMode]); - - // update editor mode to queryString - useEffect(() => { - if (editorMode) { - viewService.view.updateQueryString( - { - mode: editorMode, - }, - { - replace: true, - } - ); - } - }, [editorMode, viewService.view]); - - // set sync engine priority target - useEffect(() => { - currentWorkspace.engine.doc.setPriority(pageId, 10); - return () => { - currentWorkspace.engine.doc.setPriority(pageId, 5); - }; - }, [currentWorkspace, pageId]); - - const isInTrash = useLiveData(doc?.meta$.map(meta => meta.trash)); - - useEffect(() => { - if (doc && isInTrash) { - currentWorkspace.docCollection.awarenessStore.setReadonly( - doc.blockSuiteDoc.blockCollection, - true - ); - } - }, [currentWorkspace.docCollection.awarenessStore, doc, isInTrash]); - - // if sync engine has been synced and the page is null, show 404 page. - if (docListReady && !doc) { - return ; - } - - if (!doc || !editor) { - return ; - } - - return ( - - - - - - ); -}; - export const Component = () => { performanceRenderLogger.debug('DetailPage'); @@ -448,5 +335,9 @@ export const Component = () => { const pageId = params.pageId; - return pageId ? : null; + return pageId ? ( + + + + ) : null; }; diff --git a/packages/frontend/core/src/utils/event.ts b/packages/frontend/core/src/utils/event.ts index 71786d1698..4d0fd45750 100644 --- a/packages/frontend/core/src/utils/event.ts +++ b/packages/frontend/core/src/utils/event.ts @@ -4,6 +4,10 @@ export function stopPropagation(event: BaseSyntheticEvent) { event.stopPropagation(); } +export function preventDefault(event: BaseSyntheticEvent) { + event.preventDefault(); +} + export function stopEvent(event: BaseSyntheticEvent) { event.stopPropagation(); event.preventDefault(); diff --git a/packages/frontend/mobile/src/components/page-header/styles.css.ts b/packages/frontend/mobile/src/components/page-header/styles.css.ts index 50b998cb7c..2ea30885ab 100644 --- a/packages/frontend/mobile/src/components/page-header/styles.css.ts +++ b/packages/frontend/mobile/src/components/page-header/styles.css.ts @@ -45,5 +45,5 @@ export const prefix = style({ export const suffix = style({ display: 'flex', alignItems: 'center', - gap: 18, + gap: 6, }); diff --git a/packages/frontend/mobile/src/pages/workspace/detail.tsx b/packages/frontend/mobile/src/pages/workspace/detail.tsx deleted file mode 100644 index 937c081665..0000000000 --- a/packages/frontend/mobile/src/pages/workspace/detail.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { PageHeader } from '../../components/page-header'; - -export const Component = () => { - return ( - <> - -
/workspace/:workspaceId/:pageId
; - - ); -}; diff --git a/packages/frontend/mobile/src/pages/workspace/detail/journal-icon-button.tsx b/packages/frontend/mobile/src/pages/workspace/detail/journal-icon-button.tsx new file mode 100644 index 0000000000..f111fa9128 --- /dev/null +++ b/packages/frontend/mobile/src/pages/workspace/detail/journal-icon-button.tsx @@ -0,0 +1,43 @@ +import { IconButton, MobileMenu } from '@affine/component'; +import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; +import { EditorJournalPanel } from '@affine/core/pages/workspace/detail-page/tabs/journal'; +import { TodayIcon, TomorrowIcon, YesterdayIcon } from '@blocksuite/icons/rc'; +import { useService, WorkspaceService } from '@toeverything/infra'; + +export const JournalIconButton = ({ + docId, + className, +}: { + docId: string; + className?: string; +}) => { + const workspace = useService(WorkspaceService).workspace; + const { journalDate, isJournal } = useJournalInfoHelper( + workspace.docCollection, + docId + ); + const Icon = journalDate + ? journalDate.isBefore(new Date(), 'day') + ? YesterdayIcon + : journalDate.isAfter(new Date(), 'day') + ? TomorrowIcon + : TodayIcon + : TodayIcon; + + if (!isJournal) { + return null; + } + + return ( + } + contentOptions={{ + align: 'center', + }} + > + + + + + ); +}; diff --git a/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.css.ts b/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.css.ts new file mode 100644 index 0000000000..ad20d552c0 --- /dev/null +++ b/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.css.ts @@ -0,0 +1,91 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { globalStyle, style } from '@vanilla-extract/css'; + +export const root = style({ + background: cssVarV2('layer/background/primary'), + minHeight: '100vh', + display: 'flex', + flexDirection: 'column', +}); + +export const header = style({ + background: cssVarV2('layer/background/primary'), + position: 'sticky', + top: 0, + zIndex: 1, +}); + +export const mainContainer = style({ + containerType: 'inline-size', + display: 'flex', + flexDirection: 'column', + flex: 1, + overflow: 'hidden', + borderTop: `0.5px solid transparent`, + transition: 'border-color 0.2s', + selectors: { + '&[data-dynamic-top-border="false"]': { + borderColor: cssVar('borderColor'), + }, + '&[data-has-scroll-top="true"]': { + borderColor: cssVar('borderColor'), + }, + }, +}); + +export const editorContainer = style({ + position: 'relative', + display: 'flex', + flexDirection: 'column', + flex: 1, + zIndex: 0, +}); +// brings styles of .affine-page-viewport from blocksuite +export const affineDocViewport = style({ + display: 'flex', + flexDirection: 'column', + containerName: 'viewport', + containerType: 'inline-size', + background: cssVarV2('layer/background/primary'), + selectors: { + '&[data-mode="edgeless"]': { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + }, +}); + +export const scrollbar = style({ + marginRight: '4px', +}); + +globalStyle('.doc-title-container', { + fontSize: cssVar('fontH1'), + '@container': { + [`viewport (width <= 640px)`]: { + padding: '10px 16px', + lineHeight: '38px', + }, + }, +}); + +globalStyle('.affine-page-root-block-container', { + '@container': { + [`viewport (width <= 640px)`]: { + paddingLeft: 16, + paddingRight: 16, + }, + }, +}); + +export const journalIconButton = style({ + position: 'absolute', + zIndex: 1, + top: 16, + right: 12, + display: 'flex', +}); diff --git a/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.tsx b/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.tsx new file mode 100644 index 0000000000..de23e21331 --- /dev/null +++ b/packages/frontend/mobile/src/pages/workspace/detail/mobile-detail-page.tsx @@ -0,0 +1,225 @@ +import { IconButton } from '@affine/component'; +import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary'; +import { PageDetailEditor } from '@affine/core/components/page-detail-editor'; +import { useRegisterBlocksuiteEditorCommands } from '@affine/core/hooks/affine/use-register-blocksuite-editor-commands'; +import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor'; +import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; +import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state'; +import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; +import { EditorService } from '@affine/core/modules/editor'; +import { WorkbenchService } from '@affine/core/modules/workbench'; +import { ViewService } from '@affine/core/modules/workbench/services/view'; +import { DetailPageWrapper } from '@affine/core/pages/workspace/detail-page/detail-page-wrapper'; +import type { PageRootService } from '@blocksuite/blocks'; +import { + BookmarkBlockService, + customImageProxyMiddleware, + EmbedGithubBlockService, + EmbedLoomBlockService, + EmbedYoutubeBlockService, + ImageBlockService, +} from '@blocksuite/blocks'; +import { DisposableGroup } from '@blocksuite/global/utils'; +import { ShareIcon } from '@blocksuite/icons/rc'; +import { type AffineEditorContainer } from '@blocksuite/presets'; +import type { Doc as BlockSuiteDoc } from '@blocksuite/store'; +import { + DocService, + FrameworkScope, + GlobalContextService, + useLiveData, + useServices, + WorkspaceService, +} from '@toeverything/infra'; +import clsx from 'clsx'; +import { useCallback, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; + +import { PageHeader } from '../../../components'; +import { JournalIconButton } from './journal-icon-button'; +import * as styles from './mobile-detail-page.css'; +import { PageHeaderMenuButton } from './page-header-more-button'; + +const DetailPageImpl = () => { + const { editorService, docService, workspaceService, globalContextService } = + useServices({ + WorkbenchService, + ViewService, + EditorService, + DocService, + WorkspaceService, + GlobalContextService, + }); + const editor = editorService.editor; + const workspace = workspaceService.workspace; + const docCollection = workspace.docCollection; + const globalContext = globalContextService.globalContext; + const doc = docService.doc; + + const mode = useLiveData(editor.mode$); + + const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash)); + const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper(); + const editorContainer = useLiveData(editor.editorContainer$); + + const { setDocReadonly } = useDocMetaHelper(workspace.docCollection); + + // TODO(@eyhn): remove jotai here + const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor(); + + useEffect(() => { + setActiveBlockSuiteEditor(editorContainer); + }, [editorContainer, setActiveBlockSuiteEditor]); + + useEffect(() => { + globalContext.docId.set(doc.id); + globalContext.isDoc.set(true); + + return () => { + globalContext.docId.set(null); + globalContext.isDoc.set(false); + }; + }, [doc, globalContext]); + + useEffect(() => { + globalContext.docMode.set(mode); + + return () => { + globalContext.docMode.set(null); + }; + }, [doc, globalContext, mode]); + + useEffect(() => { + setDocReadonly(doc.id, true); + }, [doc.id, setDocReadonly]); + + useEffect(() => { + globalContext.isTrashDoc.set(!!isInTrash); + + return () => { + globalContext.isTrashDoc.set(null); + }; + }, [globalContext, isInTrash]); + + useRegisterBlocksuiteEditorCommands(editor); + const title = useLiveData(doc.title$); + usePageDocumentTitle(title); + + const onLoad = useCallback( + (_bsPage: BlockSuiteDoc, editorContainer: AffineEditorContainer) => { + // blocksuite editor host + const editorHost = editorContainer.host; + + // provide image proxy endpoint to blocksuite + editorHost?.std.clipboard.use( + customImageProxyMiddleware(runtimeConfig.imageProxyUrl) + ); + ImageBlockService.setImageProxyURL(runtimeConfig.imageProxyUrl); + + // provide link preview endpoint to blocksuite + BookmarkBlockService.setLinkPreviewEndpoint(runtimeConfig.linkPreviewUrl); + EmbedGithubBlockService.setLinkPreviewEndpoint( + runtimeConfig.linkPreviewUrl + ); + EmbedYoutubeBlockService.setLinkPreviewEndpoint( + runtimeConfig.linkPreviewUrl + ); + EmbedLoomBlockService.setLinkPreviewEndpoint( + runtimeConfig.linkPreviewUrl + ); + + // provide page mode and updated date to blocksuite + const pageService = + editorHost?.std.spec.getService('affine:page'); + const disposable = new DisposableGroup(); + if (pageService) { + disposable.add( + pageService.slots.docLinkClicked.on(({ docId, blockId }) => { + return blockId + ? jumpToPageBlock(docCollection.id, docId, blockId) + : openPage(docCollection.id, docId); + }) + ); + disposable.add( + pageService.slots.tagClicked.on(({ tagId }) => { + jumpToTag(workspace.id, tagId); + }) + ); + } + + editor.setEditorContainer(editorContainer); + + return () => { + disposable.dispose(); + }; + }, + [ + editor, + jumpToPageBlock, + docCollection.id, + openPage, + jumpToTag, + workspace.id, + ] + ); + + return ( + +
+
+ {/* Add a key to force rerender when page changed, to avoid error boundary persisting. */} + + + + +
+
+
+ ); +}; + +export const Component = () => { + const params = useParams(); + const pageId = params.pageId; + + if (!pageId) { + return null; + } + + return ( +
+ + + } + /> + + + } + /> + + +
+ ); +}; diff --git a/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.css.ts b/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.css.ts new file mode 100644 index 0000000000..a1ae246ec4 --- /dev/null +++ b/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.css.ts @@ -0,0 +1,16 @@ +import { cssVar } from '@toeverything/theme'; +import { style } from '@vanilla-extract/css'; + +export const iconButton = style({ + selectors: { + '&[data-state=open]': { + backgroundColor: cssVar('hoverColor'), + }, + }, + padding: '10px', +}); + +export const outlinePanel = style({ + maxHeight: '60vh', + overflow: 'auto', +}); diff --git a/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.tsx b/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.tsx new file mode 100644 index 0000000000..cedb3eff8b --- /dev/null +++ b/packages/frontend/mobile/src/pages/workspace/detail/page-header-more-button.tsx @@ -0,0 +1,132 @@ +import { IconButton, toast } from '@affine/component'; +import { + MenuSeparator, + MobileMenu, + MobileMenuItem, +} from '@affine/component/ui/menu'; +import { useFavorite } from '@affine/core/components/blocksuite/block-suite-header/favorite'; +import { IsFavoriteIcon } from '@affine/core/components/pure/icons'; +import { track } from '@affine/core/mixpanel'; +import { EditorService } from '@affine/core/modules/editor'; +import { EditorOutlinePanel } from '@affine/core/pages/workspace/detail-page/tabs/outline'; +import { preventDefault } from '@affine/core/utils'; +import { useI18n } from '@affine/i18n'; +import { + EdgelessIcon, + InformationIcon, + MoreHorizontalIcon, + PageIcon, + TocIcon, +} from '@blocksuite/icons/rc'; +import { useLiveData, useService } from '@toeverything/infra'; +import { useCallback } from 'react'; + +import * as styles from './page-header-more-button.css'; +import { DocInfoSheet } from './sheets/doc-info'; + +type PageMenuProps = { + docId: string; +}; + +export const PageHeaderMenuButton = ({ docId }: PageMenuProps) => { + const t = useI18n(); + + const editorService = useService(EditorService); + const editorContainer = useLiveData(editorService.editor.editorContainer$); + + const isInTrash = useLiveData( + editorService.editor.doc.meta$.map(meta => meta.trash) + ); + const currentMode = useLiveData(editorService.editor.mode$); + + const { favorite, toggleFavorite } = useFavorite(docId); + + const handleSwitchMode = useCallback(() => { + editorService.editor.toggleMode(); + track.$.header.docOptions.switchPageMode({ + mode: currentMode === 'page' ? 'edgeless' : 'page', + }); + toast( + currentMode === 'page' + ? t['com.affine.toastMessage.edgelessMode']() + : t['com.affine.toastMessage.pageMode']() + ); + }, [currentMode, editorService, t]); + + const handleMenuOpenChange = useCallback((open: boolean) => { + if (open) { + track.$.header.docOptions.open(); + } + }, []); + + const handleToggleFavorite = useCallback(() => { + track.$.header.docOptions.toggleFavorite(); + toggleFavorite(); + }, [toggleFavorite]); + + const EditMenu = ( + <> + : } + data-testid="editor-option-menu-edgeless" + onSelect={handleSwitchMode} + > + {t['Convert to ']()} + {currentMode === 'page' + ? t['com.affine.pageMode.edgeless']() + : t['com.affine.pageMode.page']()} + + } + > + {favorite + ? t['com.affine.favoritePageOperation.remove']() + : t['com.affine.favoritePageOperation.add']()} + + + }> + } + onClick={preventDefault} + > + {t['com.affine.page-properties.page-info.view']()} + + + + +
+ } + > + } onClick={preventDefault}> + {t['com.affine.header.option.view-toc']()} + + + + ); + if (isInTrash) { + return null; + } + return ( + + + + + + ); +}; diff --git a/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.css.ts b/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.css.ts new file mode 100644 index 0000000000..fe23253ea6 --- /dev/null +++ b/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.css.ts @@ -0,0 +1,51 @@ +import { rowHPadding } from '@affine/core/components/affine/page-properties/styles.css'; +import { style } from '@vanilla-extract/css'; + +export const viewport = style({ + vars: { + [rowHPadding]: '0px', + }, +}); + +export const item = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + gap: 4, + height: 34, + padding: '0 20px', + + fontSize: 17, + lineHeight: '22px', + fontWeight: 400, + letterSpacing: -0.43, +}); + +export const linksRow = style({ + padding: '0 16px', +}); + +export const timeRow = style({ + padding: '0 16px', +}); + +export const tagsRow = style({ + padding: '0 16px', +}); + +export const properties = style({ + padding: '0 16px', +}); + +export const scrollBar = style({ + width: 6, + transform: 'translateX(-4px)', +}); + +export const rowNameContainer = style({ + display: 'flex', + flexDirection: 'row', + gap: 6, + padding: 6, + width: '160px', +}); diff --git a/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.tsx b/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.tsx new file mode 100644 index 0000000000..e5e75d21b3 --- /dev/null +++ b/packages/frontend/mobile/src/pages/workspace/detail/sheets/doc-info.tsx @@ -0,0 +1,100 @@ +import { Divider, Scrollable } from '@affine/component'; +import { + PagePropertyRow, + SortableProperties, + usePagePropertiesManager, +} from '@affine/core/components/affine/page-properties'; +import { managerContext } from '@affine/core/components/affine/page-properties/common'; +import { LinksRow } from '@affine/core/components/affine/page-properties/info-modal/links-row'; +import { TagsRow } from '@affine/core/components/affine/page-properties/info-modal/tags-row'; +import { TimeRow } from '@affine/core/components/affine/page-properties/info-modal/time-row'; +import { DocsSearchService } from '@affine/core/modules/docs-search'; +import { useI18n } from '@affine/i18n'; +import { LiveData, useLiveData, useService } from '@toeverything/infra'; +import { Suspense, useMemo } from 'react'; + +import * as styles from './doc-info.css'; + +export const DocInfoSheet = ({ docId }: { docId: string }) => { + const manager = usePagePropertiesManager(docId); + const docsSearchService = useService(DocsSearchService); + const t = useI18n(); + + const links = useLiveData( + useMemo( + () => LiveData.from(docsSearchService.watchRefsFrom(docId), null), + [docId, docsSearchService] + ) + ); + const backlinks = useLiveData( + useMemo( + () => LiveData.from(docsSearchService.watchRefsTo(docId), null), + [docId, docsSearchService] + ) + ); + + return ( + + + + + + + {backlinks && backlinks.length > 0 ? ( + <> + + + + ) : null} + {links && links.length > 0 ? ( + <> + + + + ) : null} +
+ + + {properties => + properties.length ? ( + <> + {properties + .filter( + property => + manager.isPropertyRequired(property.id) || + (property.visibility !== 'hide' && + !( + property.visibility === 'hide-if-empty' && + !property.value + )) + ) + .map(property => ( + + ))} + + ) : null + } + +
+
+
+
+ +
+ ); +}; diff --git a/packages/frontend/mobile/src/router.tsx b/packages/frontend/mobile/src/router.tsx index fd48bc8a83..3e35606927 100644 --- a/packages/frontend/mobile/src/router.tsx +++ b/packages/frontend/mobile/src/router.tsx @@ -95,7 +95,7 @@ export const viewRoutes = [ }, { path: '/:pageId', - lazy: () => import('./pages/workspace/detail'), + lazy: () => import('./pages/workspace/detail/mobile-detail-page'), }, { path: '*',