diff --git a/docs/docs/dev-setup.md b/docs/docs/dev-setup.md index 6d68c1f587..bb856f476c 100644 --- a/docs/docs/dev-setup.md +++ b/docs/docs/dev-setup.md @@ -70,3 +70,24 @@ Eventually we might be able to setup all of this be configured with Nix. ## Building On Windows Internally, none of us uses Windows for development, but we _do_ build _for_ Windows _on_ Windows machines in CI. You can inspect the corresponding GitHub Actions workflow definitions to find out what needs to be installed to compile the project. + + +### Running Web Site + +#### 1. Run the Daemon + +You can start the daemon go daemon with: + +`go run ./backend/cmd/mintter-site "http://localhost:3000" -data-dir=$HOME/.mttsite -p2p.port=57000 -grpc.port=57002 -http.port=57001 -p2p.no-relay` + +Alternatively, you can do this in two steps: + +`go build -o plz-out/stellar ./backend/cmd/mintter-site` + +`./plz-out/stellar "https://mintter.com" -data-dir /root/.mttsite -p2p.port=57000 -grpc.port=57002 -http.port=57001 -p2p.no-relay` + +### 2. Start the Next.js Web App + +In the Mintter directory, start by running `yarn`. Then: + +`HM_BASE_URL="http://localhost:3000" NEXT_PUBLIC_GRPC_HOST="http://localhost:57001" PORT=3000 yarn site` \ No newline at end of file diff --git a/frontend/apps/site/account-page.tsx b/frontend/apps/site/account-page.tsx index c0cf6cae6e..0be51aadd1 100644 --- a/frontend/apps/site/account-page.tsx +++ b/frontend/apps/site/account-page.tsx @@ -43,11 +43,10 @@ function isEmptyObject(obj: unknown) { } export default function AccountPage({accountId}: {accountId: string}) { - const publication = trpc.account.get.useQuery({ + const query = trpc.account.get.useQuery({ accountId, }) - - const account = publication.data?.account + const account = query.data?.account return ( @@ -58,9 +57,9 @@ export default function AccountPage({accountId}: {accountId: string}) { - {account && publication.isSuccess ? ( + {account && query.isSuccess ? ( - ) : publication.isLoading ? ( + ) : query.isLoading ? ( ) : ( diff --git a/frontend/apps/site/pages/[pageSlug].tsx b/frontend/apps/site/pages/[pageSlug].tsx deleted file mode 100644 index aafa99ffe2..0000000000 --- a/frontend/apps/site/pages/[pageSlug].tsx +++ /dev/null @@ -1,15 +0,0 @@ -import {GetServerSideProps} from 'next' -import PublicationSlugPage, {PubSlugPageProps} from 'publication-slug-page' -import {prepareSlugPage} from 'server/page-slug' -import {EveryPageProps} from './_app' - -export default function PathPublicationPage(props: {pathName: string}) { - return -} - -export const getServerSideProps: GetServerSideProps< - EveryPageProps & PubSlugPageProps -> = async (context) => { - const pathName = (context.params?.pageSlug as string) || '' - return await prepareSlugPage(context, pathName) -} diff --git a/frontend/apps/site/pages/[pathName].tsx b/frontend/apps/site/pages/[pathName].tsx new file mode 100644 index 0000000000..7681724d16 --- /dev/null +++ b/frontend/apps/site/pages/[pathName].tsx @@ -0,0 +1,21 @@ +import {GetServerSideProps} from 'next' +import {PubSlugPageProps} from 'publication-slug-page' +import {EveryPageProps} from './_app' +import GroupPublicationPage from './g/[groupEid]/[pathName]' +import {getSiteGroup} from 'server/site-info' +import {getGroupPathNamePageProps, getGroupView} from 'server/group' + +const PathPublicationPage = GroupPublicationPage +export default PathPublicationPage + +export const getServerSideProps: GetServerSideProps< + EveryPageProps & PubSlugPageProps +> = async (context) => { + const pathName = (context.params?.pathName as string) || '' + const {groupEid} = await getSiteGroup() + return await getGroupPathNamePageProps({ + groupEid, + pathName, + context, + }) +} diff --git a/frontend/apps/site/pages/api/site-info.tsx b/frontend/apps/site/pages/api/site-info.tsx deleted file mode 100644 index d368fe2191..0000000000 --- a/frontend/apps/site/pages/api/site-info.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// test page for groupsClient.getSiteInfo({ hostname: process.env.HM_BASE_URL }) so we can see if this works in production -import {groupsClient} from 'client' -import {NextApiRequest, NextApiResponse} from 'next' - -const gatewayHostWithProtocol = process.env.HM_BASE_URL -const gatewayHost = new URL(gatewayHostWithProtocol || '').hostname - -console.log('ℹ️ site info! ', { - gatewayHost, - gatewayHostWithProtocol, - port: process.env.PORT, - grpcHost: process.env.NEXT_PUBLIC_GRPC_HOST, -}) - -export default async function siteTestHandler( - req: NextApiRequest, - res: NextApiResponse, -) { - const info = await groupsClient.getSiteInfo({ - hostname: gatewayHostWithProtocol, - }) - - res - .status(200) - .setHeader('Content-Type', 'application/json') - .send(JSON.stringify(info.toJson(), null, 2)) -} diff --git a/frontend/apps/site/pages/d/[docEid].tsx b/frontend/apps/site/pages/d/[docEid].tsx index 3269de9406..4004fff537 100644 --- a/frontend/apps/site/pages/d/[docEid].tsx +++ b/frontend/apps/site/pages/d/[docEid].tsx @@ -9,19 +9,6 @@ import {useRequiredRouteQuery, useRouteQuery} from 'server/router-queries' import {getPageProps, serverHelpers} from 'server/ssr-helpers' import {createHmId} from '@mintter/shared' -function getDocSlugUrl( - pathName: string | undefined, - docId: string, - versionId?: string, - blockRef?: string, -) { - let url = `/d/${docId}` - if (pathName) url = pathName === '/' ? '/' : `/${pathName}` - if (versionId) url += `?v=${versionId}` - if (blockRef) url += `#${blockRef}` - return url -} - export default function IDPublicationPage( props: InferGetServerSidePropsType, ) { @@ -47,21 +34,6 @@ export const getServerSideProps: GetServerSideProps = async ( const helpers = serverHelpers({}) - // if (docRecord) { - // // old redirect to pretty URL behavior - // return { - // redirect: { - // temporary: true, - // destination: getDocSlugUrl( - // docRecord.path, - // docId, - // version || docRecord.versionId, - // ), - // }, - // props: {}, - // } as const - // } - // await impatiently( // helpers.publication.get.prefetch({ // documentId: docId, diff --git a/frontend/apps/site/pages/g/[groupEid]/[pathName].tsx b/frontend/apps/site/pages/g/[groupEid]/[pathName].tsx index ead544d6ff..04e97274b3 100644 --- a/frontend/apps/site/pages/g/[groupEid]/[pathName].tsx +++ b/frontend/apps/site/pages/g/[groupEid]/[pathName].tsx @@ -1,12 +1,9 @@ -import {createHmId} from '@mintter/shared' +import {getGroupPathNamePageProps} from 'server/group' import {Heading, Spinner} from '@mintter/ui' -import {daemonClient, networkingClient} from 'client' import {GetServerSideProps} from 'next' import {EveryPageProps} from 'pages/_app' import PublicationPage from 'publication-page' -import {setAllowAnyHostGetCORS} from 'server/cors' import {useRouteQuery} from 'server/router-queries' -import {getPageProps, serverHelpers} from 'server/ssr-helpers' import {trpc} from 'trpc' export type GroupPubPageProps = { @@ -40,23 +37,9 @@ export const getServerSideProps: GetServerSideProps< > = async (context) => { const pathName = (context.params?.pathName as string) || '' const groupEid = (context.params?.groupEid as string) || '' - const groupId = createHmId('g', groupEid) - const helpers = serverHelpers({}) - - setAllowAnyHostGetCORS(context.res) - - const info = await daemonClient.getInfo({}) - const peerInfo = await networkingClient.getPeerInfo({ - deviceId: info.deviceId, + return await getGroupPathNamePageProps({ + groupEid, + pathName, + context, }) - context.res.setHeader( - 'x-mintter-site-p2p-addresses', - peerInfo.addrs.join(','), - ) - const group = await helpers.group.get.fetch({groupId}) - const groupContent = await helpers.group.listContent.fetch({groupId}) - - return { - props: await getPageProps(helpers, {pathName, groupId}), - } } diff --git a/frontend/apps/site/pages/g/[groupEid]/index.tsx b/frontend/apps/site/pages/g/[groupEid]/index.tsx index 7c955a60fa..7e7632e3c4 100644 --- a/frontend/apps/site/pages/g/[groupEid]/index.tsx +++ b/frontend/apps/site/pages/g/[groupEid]/index.tsx @@ -10,6 +10,8 @@ import {SiteHead} from '../../../site-head' import {Timestamp} from '@bufbuild/protobuf' import { + Publication, + UnpackedHypermediaId, createHmId, createPublicWebHmUrl, formattedDate, @@ -29,11 +31,13 @@ import { } from '@mintter/ui' import {AccountAvatarLink, AccountRow} from 'components/account-row' import {format} from 'date-fns' -import {ReactElement} from 'react' +import {ReactElement, ReactNode} from 'react' import {GestureResponderEvent} from 'react-native' import {Paragraph} from 'tamagui' import {HMGroup, HMPublication} from '../../../server/json-hm' import {trpc} from '../../../trpc' +import {GroupView, getGroupPageProps, getGroupView} from '../../../server/group' +import {PublicationContent} from '../../../publication-page' function GroupOwnerSection({owner}: {owner: string}) { return ( @@ -166,13 +170,33 @@ function GroupContentItem({ ) } -export default function GroupPage({ - groupId, - version, -}: InferGetServerSidePropsType) { +function FrontDoc({ + item, +}: { + item: + | { + version: string + pathName: string + publication: HMPublication | null + docId: UnpackedHypermediaId & {docId: string} + } + | null + | undefined +}) { + if (!item?.publication) return Not Found + return +} + +export type GroupPageProps = { + groupId: string + version?: string + view: GroupView +} + +export default function GroupPage({groupId, version, view}: GroupPageProps) { const group = trpc.group.get.useQuery({ groupId, - //version + version, }) const groupContent = trpc.group.listContent.useQuery({ groupId, @@ -180,6 +204,43 @@ export default function GroupPage({ const loadedGroup = group.data?.group + const listView = groupContent.data + ? groupContent.data.map((contentItem) => { + if (contentItem?.pathName === '/') return null + return ( + contentItem && ( + + ) + ) + }) + : null + + let mainView: ReactNode = listView + + const frontPageItem = groupContent.data?.find( + (item) => item?.pathName === '/', + ) + + const frontDocView = + + if (view === 'front') { + mainView = frontDocView + } else if (view === 'list') { + mainView = listView + } else if (frontPageItem) { + mainView = ( + <> + {frontDocView} + {listView} + + ) + } else { + mainView = listView + } return ( @@ -234,19 +295,7 @@ export default function GroupPage({ {loadedGroup?.description} ) : null} - {groupContent.data - ? groupContent.data.map((contentItem) => { - return ( - contentItem && ( - - ) - ) - }) - : null} + {mainView} @@ -263,26 +312,8 @@ export const getServerSideProps: GetServerSideProps = async ( context: GetServerSidePropsContext, ) => { const {params, query} = context - const groupEid = params?.groupId ? String(params.groupId) : undefined - const groupId = groupEid ? createHmId('g', groupEid) : undefined - - let version = query.v ? String(query.v) : null - - setAllowAnyHostGetCORS(context.res) - - if (!groupId) return {notFound: true} as const - - const helpers = serverHelpers({}) - - const groupRecord = await helpers.group.get.fetch({ - groupId, - }) - - await helpers.group.listContent.prefetch({ - groupId, - }) - - return { - props: await getPageProps(helpers, {groupId, version}), - } + const groupEid = params?.groupEid ? String(params.groupEid) : undefined + if (!groupEid) return {notFound: true} + const view = getGroupView(query.view) + return await getGroupPageProps({groupEid, context, view}) } diff --git a/frontend/apps/site/pages/index.tsx b/frontend/apps/site/pages/index.tsx index c753bc42a4..c53791dcb6 100644 --- a/frontend/apps/site/pages/index.tsx +++ b/frontend/apps/site/pages/index.tsx @@ -1,22 +1,19 @@ import {GetServerSideProps} from 'next' -import PublicationSlugPage, {PubSlugPageProps} from 'publication-slug-page' -import {prepareSlugPage} from 'server/page-slug' +import {PubSlugPageProps} from 'publication-slug-page' import {EveryPageProps} from './_app' +import {getGroupPageProps, getGroupView} from 'server/group' +import {getSiteGroup} from 'server/site-info' +import GroupPage, {GroupPageProps} from './g/[groupEid]' -// // Temp Mintter home screen document: -// let fallbackDocId = process.env.MINTTER_HOME_PUBID || 'mnoboS11GwRlRAh2dhYlTw' -// let fallbackVersion = -// process.env.MINTTER_HOME_VERSION || -// 'bafy2bzacednwllikmc7rittnmz4s7cfpo3p2ldsap3bcmgxp7cdpzhoiu5w' - -// //https://mintter.com/p/mnoboS11GwRlRAh2dhYlTw?v=bafy2bzacednwllikmc7rittnmz4s7cfpo3p2ldsap3bcmgxp7cdpzhoiu5w - -export default function HomePage(props: {pathName: string}) { - return +export default function HomePage(props: GroupPageProps) { + return } export const getServerSideProps: GetServerSideProps< EveryPageProps & PubSlugPageProps > = async (context) => { - return await prepareSlugPage(context, '/') + const {groupEid} = await getSiteGroup() + console.log('serer side', context.query) + const view = getGroupView(context.query.view) + return await getGroupPageProps({groupEid, context, view}) } diff --git a/frontend/apps/site/publication-metadata.tsx b/frontend/apps/site/publication-metadata.tsx index 173148346e..55382c413c 100644 --- a/frontend/apps/site/publication-metadata.tsx +++ b/frontend/apps/site/publication-metadata.tsx @@ -3,6 +3,7 @@ import { createPublicWebHmUrl, formattedDate, HMTimestamp, + idToUrl, pluralS, unpackDocId, } from '@mintter/shared' @@ -382,11 +383,15 @@ function CitationPreview({citationLink}: {citationLink: HMLink}) { ) if (!sourcePub.data) return null if (!source?.documentId) return null + const destUrl = idToUrl( + source?.documentId, + null, + source?.version, + source?.blockId, + ) + if (!destUrl) return null return ( - + {sourcePub.data?.publication?.document?.title} ) @@ -508,14 +513,6 @@ export function PublicationMetadata({ ) } -function getDocUrl(docId: string, versionId?: string, blockRef?: string) { - // todo centralize this url creation logic better - let url = `/d/${docId}` - if (versionId) url += `?v=${versionId}` - if (blockRef) url += `#${blockRef}` - return url -} - function getDocSlugUrl( pathName: string | undefined, docId: string, @@ -605,9 +602,13 @@ export function PublishedMeta({ Published: {publishTimeRelative}{' '} diff --git a/frontend/apps/site/publication-page.tsx b/frontend/apps/site/publication-page.tsx index 22ad3e9bb4..d8981a2f60 100644 --- a/frontend/apps/site/publication-page.tsx +++ b/frontend/apps/site/publication-page.tsx @@ -58,7 +58,7 @@ export type PublicationPageData = { siteInfo: SiteInfo | null } -function PublicationContent({ +export function PublicationContent({ publication, }: { publication: HMPublication | undefined diff --git a/frontend/apps/site/server/group.ts b/frontend/apps/site/server/group.ts new file mode 100644 index 0000000000..1f5245d7ed --- /dev/null +++ b/frontend/apps/site/server/group.ts @@ -0,0 +1,86 @@ +import {createHmId} from '@mintter/shared' +import {getPageProps, serverHelpers} from './ssr-helpers' +import {setAllowAnyHostGetCORS} from './cors' +import {GetServerSidePropsContext} from 'next' +import {daemonClient, networkingClient} from '../client' + +export type GroupView = 'front' | 'list' | null + +export function getGroupView(input: string | string[] | undefined): GroupView { + if (input === 'front') return 'front' + if (input === 'list') return 'list' + return null +} + +export async function getGroupPathNamePageProps({ + groupEid, + pathName, + context, +}: { + groupEid: string + pathName: string + context: GetServerSidePropsContext +}) { + const groupId = createHmId('g', groupEid) + const helpers = serverHelpers({}) + + setAllowAnyHostGetCORS(context.res) + + const info = await daemonClient.getInfo({}) + const peerInfo = await networkingClient.getPeerInfo({ + deviceId: info.deviceId, + }) + context.res.setHeader( + 'x-mintter-site-p2p-addresses', + peerInfo.addrs.join(','), + ) + const group = await helpers.group.get.fetch({groupId}) + const groupContent = await helpers.group.listContent.fetch({groupId}) + + return { + props: await getPageProps(helpers, {pathName, groupId}), + } +} + +export async function getGroupPageProps({ + groupEid, + context, + view, +}: { + groupEid: string + context: GetServerSidePropsContext + view: GroupView +}) { + const {params, query} = context + const groupId = groupEid ? createHmId('g', groupEid) : undefined + + let version = query.v ? String(query.v) : null + + setAllowAnyHostGetCORS(context.res) + + if (!groupId) return {notFound: true} as const + + const helpers = serverHelpers({}) + + const groupRecord = await helpers.group.get.fetch({ + groupId, + }) + const members = await helpers.group.listMembers.fetch({ + groupId, + }) + await Promise.all( + members.map((member) => + helpers.account.get.fetch({ + accountId: member.account, + }), + ), + ) + + const content = await helpers.group.listContent.fetch({ + groupId, + }) + + return { + props: await getPageProps(helpers, {groupId, version, pathName: '/', view}), + } +} diff --git a/frontend/apps/site/server/routers/_app.ts b/frontend/apps/site/server/routers/_app.ts index 55f2ab32f4..6908fc0777 100644 --- a/frontend/apps/site/server/routers/_app.ts +++ b/frontend/apps/site/server/routers/_app.ts @@ -57,30 +57,16 @@ const publicationRouter = router({ if (!input.documentId) { return {publication: null} } - const alreadyPub = publicationsClient + const resolvedPub = await publicationsClient .getPublication({ documentId: input.documentId, version: input.versionId || '', }) .catch((e) => undefined) - const remoteSync = entitiesClient - .discoverEntity({ - id: input.documentId, - version: input.versionId || '', - }) - .then(() => undefined) - let resolvedPub = await Promise.race([alreadyPub, remoteSync]) - if (!resolvedPub) { - // the remote Sync may have just found it. so we want to re-fetch - resolvedPub = await publicationsClient.getPublication({ - documentId: input.documentId, - version: input.versionId || '', - }) - } + if (!resolvedPub) { return {publication: null} } - return {publication: null} return { publication: hmPublication(resolvedPub) || null, } @@ -206,20 +192,6 @@ const publicationRouter = router({ }) const groupRouter = router({ - getSitePath: procedure - .input(z.object({hostname: z.string()})) - .query(async ({input}) => { - // todo. get current group content and find the pathName, return the corresponding doc - console.log('getting site info') - // const siteInfo = await groupsClient.getGroup({ - // hostname: input.hostname, - // }) - // return { - // groupId: siteInfo.groupId, - // ownerId: siteInfo.ownerId, - // version: siteInfo.version, - // } - }), getGroupPath: procedure .input( z.object({ @@ -229,9 +201,7 @@ const groupRouter = router({ }), ) .query(async ({input: {pathName, groupId, version}}) => { - // todo. get current group content and find the pathName, return the corresponding doc - console.log('getting site info') - const siteInfo = await groupsClient.listContent({ + const groupContent = await groupsClient.listContent({ id: groupId, version, }) @@ -239,7 +209,7 @@ const groupRouter = router({ id: groupId, version, }) - const item = siteInfo.content[pathName] + const item = groupContent.content[pathName] if (!item) return null const itemId = unpackDocId(item) if (!itemId?.version) return null // version is required for group content @@ -260,14 +230,14 @@ const groupRouter = router({ .input( z.object({ groupId: z.string(), + version: z.string().optional(), }), ) .query(async ({input}) => { - console.log('will getGroup with id', input) const group = await groupsClient.getGroup({ id: input.groupId, + version: input.version, }) - console.log('did get group', hmGroup(group)) return { group: hmGroup(group), } diff --git a/frontend/apps/site/server/site-info.ts b/frontend/apps/site/server/site-info.ts new file mode 100644 index 0000000000..cf7886d0f3 --- /dev/null +++ b/frontend/apps/site/server/site-info.ts @@ -0,0 +1,13 @@ +// const gatewayHostWithProtocol = process.env.HM_BASE_URL +// const gatewayHost = new URL(gatewayHostWithProtocol || '').hostname + +export async function getSiteGroup(): Promise<{ + groupEid: string + version?: string | null +}> { + // todo, query for site group + return { + groupEid: '8ctYvUJwv9kmpjCT4RjBeD', + version: null, + } +} diff --git a/frontend/packages/app/src/components/contacts-prompt.tsx b/frontend/packages/app/src/components/contacts-prompt.tsx index e4fa975bd0..43257e0142 100644 --- a/frontend/packages/app/src/components/contacts-prompt.tsx +++ b/frontend/packages/app/src/components/contacts-prompt.tsx @@ -25,7 +25,26 @@ function AddConnectionForm(props: {onClose: () => void}) { if (peer) { const connectionRegexp = /connect-peer\/([\w\d]+)/ const parsedConnectUrl = peer.match(connectionRegexp) - const connectionDeviceId = parsedConnectUrl ? parsedConnectUrl[1] : null + let connectionDeviceId = parsedConnectUrl ? parsedConnectUrl[1] : null + if (!connectionDeviceId && peer.match(/^(https:\/\/)/)) { + // in this case, the "peer" input is not https://site/connect-peer/x url, but it is a web url. So lets try to connect to this site via its well known peer id. + const peerUrl = new URL(peer) + peerUrl.search = '' + peerUrl.hash = '' + peerUrl.pathname = '/.well-known/hypermedia-site' + const peerWellKnown = peerUrl.toString() + const wellKnownData = await fetch(peerWellKnown) + .then((res) => res.json()) + .catch((err) => { + console.error('Connect Error:', err) + return null + }) + if (wellKnownData?.peerInfo?.peerId) { + connectionDeviceId = wellKnownData.peerInfo.peerId + } else { + throw new Error('Failed to connet to web url: ' + peer) + } + } const addrs = connectionDeviceId ? [connectionDeviceId] : peer.trim().split(',') diff --git a/frontend/packages/app/src/pages/contacts-page.tsx b/frontend/packages/app/src/pages/contacts-page.tsx index 33642412e2..9d35d1b299 100644 --- a/frontend/packages/app/src/pages/contacts-page.tsx +++ b/frontend/packages/app/src/pages/contacts-page.tsx @@ -114,15 +114,18 @@ export default function ContactsPage() { } if (allAccounts.length === 0) { return ( - - - - - You have no Contacts yet. - - - - + <> + + + + + You have no Contacts yet. + + + + +