From 989b4461e7b1910074a664ce9af1c007fc1f1a84 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Wed, 18 Oct 2023 15:25:46 -0700 Subject: [PATCH] ChatLink: move to /link/chat, update DB, cleanups --- pages/link/chat/[linkId].tsx | 18 +++ pages/shared/[sharedId].tsx | 14 --- prisma/schema.prisma | 24 ++-- src/apps/chat/trade/TradeModal.tsx | 4 +- .../chat/trade/{ => export}/ExportChats.tsx | 114 ++++++++++-------- .../ExportedChatLink.tsx} | 16 +-- .../ExportedPublish.tsx} | 4 +- .../chat/trade/{ => import}/ImportChats.tsx | 5 +- .../trade/{ => import}/ImportOutcomeModal.tsx | 0 .../{share.server.ts => storage.server.ts} | 102 +++++++++------- src/apps/chat/trade/server/trade.router.ts | 18 +-- src/apps/chat/trade/trade.client.ts | 2 +- .../AppShared.tsx => link/AppChatLink.tsx} | 62 +++++----- .../ViewChatLink.tsx} | 13 +- src/common/routes.ts | 4 +- src/common/state/store-ui.ts | 2 +- 16 files changed, 217 insertions(+), 185 deletions(-) create mode 100644 pages/link/chat/[linkId].tsx delete mode 100644 pages/shared/[sharedId].tsx rename src/apps/chat/trade/{ => export}/ExportChats.tsx (67%) rename src/apps/chat/trade/{ExportSharedModal.tsx => export/ExportedChatLink.tsx} (89%) rename src/apps/chat/trade/{ExportPublishedModal.tsx => export/ExportedPublish.tsx} (92%) rename src/apps/chat/trade/{ => import}/ImportChats.tsx (97%) rename src/apps/chat/trade/{ => import}/ImportOutcomeModal.tsx (100%) rename src/apps/chat/trade/server/{share.server.ts => storage.server.ts} (55%) rename src/apps/{shared/AppShared.tsx => link/AppChatLink.tsx} (56%) rename src/apps/{shared/ViewSharedConversation.tsx => link/ViewChatLink.tsx} (94%) diff --git a/pages/link/chat/[linkId].tsx b/pages/link/chat/[linkId].tsx new file mode 100644 index 0000000000..aa404cb5fd --- /dev/null +++ b/pages/link/chat/[linkId].tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { useRouter } from 'next/router'; + +import { AppChatLink } from '../../../src/apps/link/AppChatLink'; + +import { AppLayout } from '~/common/layout/AppLayout'; + + +export default function ChatLinkPage() { + const { query } = useRouter(); + const linkId = query?.linkId as string ?? ''; + + return ( + + + + ); +} \ No newline at end of file diff --git a/pages/shared/[sharedId].tsx b/pages/shared/[sharedId].tsx deleted file mode 100644 index cd70c94ef9..0000000000 --- a/pages/shared/[sharedId].tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; - -import { AppShared } from '../../src/apps/shared/AppShared'; - -import { AppLayout } from '~/common/layout/AppLayout'; - - -export default function SharedViewerPage() { - return ( - - - - ); -} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 04caecfdc7..dc41dc4a22 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,15 +20,19 @@ datasource db { directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection } -model Sharing { +// +// Storage of Linked Data +// +model LinkStorage { id String @id @default(uuid()) - ownerId String - isPublic Boolean @default(true) + ownerId String + visibility LinkStorageVisibility - dataType SharingDataType - dataSize Int - data Json + dataType LinkStorageDataType + dataTitle String? + dataSize Int + data Json upVotes Int @default(0) downVotes Int @default(0) @@ -48,6 +52,12 @@ model Sharing { updatedAt DateTime @updatedAt } -enum SharingDataType { +enum LinkStorageVisibility { + PUBLIC + UNLISTED + PRIVATE +} + +enum LinkStorageDataType { CHAT_V1 } diff --git a/src/apps/chat/trade/TradeModal.tsx b/src/apps/chat/trade/TradeModal.tsx index 202ba6a0a3..03b7d9e860 100644 --- a/src/apps/chat/trade/TradeModal.tsx +++ b/src/apps/chat/trade/TradeModal.tsx @@ -4,8 +4,8 @@ import { Divider } from '@mui/joy'; import { GoodModal } from '~/common/components/GoodModal'; -import { ExportConfig, ExportChats } from './ExportChats'; -import { ImportConfig, ImportConversations } from './ImportChats'; +import { ExportConfig, ExportChats } from './export/ExportChats'; +import { ImportConfig, ImportConversations } from './import/ImportChats'; export type TradeConfig = ImportConfig | ExportConfig; diff --git a/src/apps/chat/trade/ExportChats.tsx b/src/apps/chat/trade/export/ExportChats.tsx similarity index 67% rename from src/apps/chat/trade/ExportChats.tsx rename to src/apps/chat/trade/export/ExportChats.tsx index 6b24712c11..3a65aa4311 100644 --- a/src/apps/chat/trade/ExportChats.tsx +++ b/src/apps/chat/trade/export/ExportChats.tsx @@ -10,13 +10,15 @@ import { Brand } from '~/common/brand'; import { ConfirmationModal } from '~/common/components/ConfirmationModal'; import { Link } from '~/common/components/Link'; import { apiAsyncNode } from '~/common/util/trpc.client'; -import { useChatStore } from '~/common/state/store-chats'; +import { conversationTitle, useChatStore } from '~/common/state/store-chats'; import { useUICounter, useUIPreferencesStore } from '~/common/state/store-ui'; -import type { PublishedSchema, SharePutSchema } from './server/trade.router'; -import { ExportPublishedModal } from './ExportPublishedModal'; -import { ExportSharedModal } from './ExportSharedModal'; -import { conversationToJsonV1, conversationToMarkdown, downloadAllConversationsJson, downloadConversationJson } from './trade.client'; +import type { PublishedSchema, StoragePutSchema } from '../server/trade.router'; + +import { conversationToJsonV1, conversationToMarkdown, downloadAllConversationsJson, downloadConversationJson } from '../trade.client'; + +import { ExportedChatLink } from './ExportedChatLink'; +import { ExportedPublish } from './ExportedPublish'; // global flag to enable/disable the sharing mechanics @@ -43,52 +45,52 @@ function findConversation(conversationId: string) { /** * Export Buttons and functionality - * Supports Share locally, Pulish to Paste.gg and Download in own format */ export function ExportChats(props: { config: ExportConfig, onClose: () => void }) { // state const [downloadedState, setDownloadedState] = React.useState<'ok' | 'fail' | null>(null); const [downloadedAllState, setDownloadedAllState] = React.useState<'ok' | 'fail' | null>(null); - const [shareConversationId, setShareConversationId] = React.useState(null); - const [shareUploading, setShareUploading] = React.useState(false); - const [shareResponse, setShareResponse] = React.useState(null); + const [chatLinkConfirmId, setChatLinkConfirmId] = React.useState(null); + const [chatLinkUploading, setChatLinkUploading] = React.useState(false); + const [chatLinkResponse, setChatLinkResponse] = React.useState(null); const [publishConversationId, setPublishConversationId] = React.useState(null); const [publishUploading, setPublishUploading] = React.useState(false); const [publishResponse, setPublishResponse] = React.useState(null); // external state - const { novel: shareWebBadge, touch: shareWebTouch } = useUICounter('share-web'); + const { novel: chatLinkBadge, touch: clearChatLinkBadge } = useUICounter('share-chat-link'); - // share + // chat link - const handleShareConversation = () => setShareConversationId(props.config.conversationId); + const handleChatLinkCreate = () => setChatLinkConfirmId(props.config.conversationId); - const handleConfirmedShare = async () => { - if (!shareConversationId) return; + const handleChatLinkConfirmed = async () => { + if (!chatLinkConfirmId) return; - const conversation = findConversation(shareConversationId); - setShareConversationId(null); + const conversation = findConversation(chatLinkConfirmId); + setChatLinkConfirmId(null); if (!conversation) return; - setShareUploading(true); + setChatLinkUploading(true); try { const chatV1 = conversationToJsonV1(conversation); - const response: SharePutSchema = await apiAsyncNode.trade.sharePut.mutate({ + const response: StoragePutSchema = await apiAsyncNode.trade.storagePut.mutate({ ownerId: undefined, // TODO: save owner id and reuse every time dataType: 'CHAT_V1', + dataTitle: conversationTitle(conversation) || undefined, dataObject: chatV1, }); - setShareResponse(response); - shareWebTouch(); + setChatLinkResponse(response); + clearChatLinkBadge(); } catch (error: any) { - setShareResponse({ + setChatLinkResponse({ type: 'error', error: error?.message ?? error?.toString() ?? 'unknown error', }); } - setShareUploading(false); + setChatLinkUploading(false); }; @@ -154,16 +156,18 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void } Share or download this conversation - {ENABLE_SHARING && - - } + {ENABLE_SHARING && ( + + + + )} - {/* [share] confirmation */} - {ENABLE_SHARING && shareConversationId && ( + + {/* [chat link] confirmation */} + {ENABLE_SHARING && !!chatLinkConfirmId && ( setShareConversationId(null)} onPositive={handleConfirmedShare} + open onClose={() => setChatLinkConfirmId(null)} onPositive={handleChatLinkConfirmed} title='Upload Confirmation' confirmationText={<> - Everyone with the link will be able to see this chat. + Everyone who has the unlisted link will be able to access this chat. It will be automatically deleted after 30 days. For more information, please see the privacy policy of this server.
- Are you sure you want to proceed? - } positiveActionText={'Yes, create shared link'} + Do you wish to continue? + } positiveActionText={'Yes, Create Link'} /> )} - {/* [share] outcome */} - {!!shareResponse && setShareResponse(null)} response={shareResponse} />} + {/* [chat link] response */} + {ENABLE_SHARING && !!chatLinkResponse && ( + setChatLinkResponse(null)} response={chatLinkResponse} /> + )} + {/* [publish] confirmation */} - {publishConversationId && setPublishConversationId(null)} onPositive={handlePublishConfirmed} - confirmationText={<> - Share your conversation anonymously on paste.gg? - It will be unlisted and available to share and read for 30 days. Keep in mind, deletion may not be possible. - Are you sure you want to proceed? - } positiveActionText={'Understood, upload to paste.gg'} - />} - - {/* [publish] outcome */} - {!!publishResponse && } + {publishConversationId && ( + setPublishConversationId(null)} onPositive={handlePublishConfirmed} + confirmationText={<> + Share your conversation anonymously on paste.gg? + It will be unlisted and available to share and read for 30 days. Keep in mind, deletion may not be possible. + Do you wish to continue? + } positiveActionText={'Understood, Upload to Paste.gg'} + /> + )} + + {/* [publish] response */} + {!!publishResponse && ( + + )} ; } \ No newline at end of file diff --git a/src/apps/chat/trade/ExportSharedModal.tsx b/src/apps/chat/trade/export/ExportedChatLink.tsx similarity index 89% rename from src/apps/chat/trade/ExportSharedModal.tsx rename to src/apps/chat/trade/export/ExportedChatLink.tsx index 12c7927252..b209d60a56 100644 --- a/src/apps/chat/trade/ExportSharedModal.tsx +++ b/src/apps/chat/trade/export/ExportedChatLink.tsx @@ -15,21 +15,21 @@ import { InlineError } from '~/common/components/InlineError'; import { Link } from '~/common/components/Link'; import { apiAsyncNode } from '~/common/util/trpc.client'; import { copyToClipboard } from '~/common/util/copyToClipboard'; +import { getChatLinkRelativePath } from '~/common/routes'; import { getOriginUrl } from '~/common/util/urlUtils'; -import { getSharingRelativePath } from '~/common/routes'; import { webShare, webSharePresent } from '~/common/util/pwaUtils'; -import { type ShareDeleteSchema, type SharePutSchema } from './server/trade.router'; +import { type StorageDeleteSchema, type StoragePutSchema } from '../server/trade.router'; -export function ExportSharedModal(props: { onClose: () => void, response: SharePutSchema, open: boolean }) { +export function ExportedChatLink(props: { onClose: () => void, response: StoragePutSchema, open: boolean }) { // state const [opened, setOpened] = React.useState(false); const [copied, setCopied] = React.useState(false); const [shared, setShared] = React.useState(false); const [confirmDeletion, setConfirmDeletion] = React.useState(false); - const [deletionResponse, setDeletionResponse] = React.useState(null); + const [deletionResponse, setDeletionResponse] = React.useState(null); // in case of 'put' error, just display the message if (props.response.type === 'error') { @@ -41,8 +41,8 @@ export function ExportSharedModal(props: { onClose: () => void, response: ShareP } // success - const { sharedId, deletionKey, expiresAt } = props.response; - const relativeUrl = getSharingRelativePath(sharedId); + const { objectId, deletionKey, expiresAt } = props.response; + const relativeUrl = getChatLinkRelativePath(objectId); const fullUrl = getOriginUrl() + relativeUrl; @@ -53,7 +53,7 @@ export function ExportSharedModal(props: { onClose: () => void, response: ShareP setCopied(true); }; - const onShare = async () => webShare(Brand.Title.Common, 'Check out this chat!', fullUrl, + const onShare = async () => webShare(Brand.Title.Base, 'Check out this chat!', fullUrl, () => setShared(true)); @@ -62,7 +62,7 @@ export function ExportSharedModal(props: { onClose: () => void, response: ShareP const onDeleteCancelled = () => setConfirmDeletion(false); const onConfirmedDeletion = async () => { - const result: ShareDeleteSchema = await apiAsyncNode.trade.shareDelete.mutate({ sharedId, deletionKey }); + const result: StorageDeleteSchema = await apiAsyncNode.trade.storageDelete.mutate({ objectId, deletionKey }); setDeletionResponse(result); setConfirmDeletion(false); }; diff --git a/src/apps/chat/trade/ExportPublishedModal.tsx b/src/apps/chat/trade/export/ExportedPublish.tsx similarity index 92% rename from src/apps/chat/trade/ExportPublishedModal.tsx rename to src/apps/chat/trade/export/ExportedPublish.tsx index 29bf462c33..d5a2264541 100644 --- a/src/apps/chat/trade/ExportPublishedModal.tsx +++ b/src/apps/chat/trade/export/ExportedPublish.tsx @@ -4,14 +4,14 @@ import { Alert, Box, Button, Divider, Input, Modal, ModalDialog, Stack, Typograp import { Link } from '~/common/components/Link'; -import type { PublishedSchema } from './server/trade.router'; +import type { PublishedSchema } from '../server/trade.router'; /** * Displays the result of a Paste.gg paste as a modal dialog. * This is to give the user the chance to write down the deletion key, mainly. */ -export function ExportPublishedModal(props: { onClose: () => void, response: PublishedSchema, open: boolean }) { +export function ExportedPublish(props: { onClose: () => void, response: PublishedSchema, open: boolean }) { if (!props.response || !props.response.url) return null; diff --git a/src/apps/chat/trade/ImportChats.tsx b/src/apps/chat/trade/import/ImportChats.tsx similarity index 97% rename from src/apps/chat/trade/ImportChats.tsx rename to src/apps/chat/trade/import/ImportChats.tsx index 5e9bb0276c..7b04a1caa7 100644 --- a/src/apps/chat/trade/ImportChats.tsx +++ b/src/apps/chat/trade/import/ImportChats.tsx @@ -11,9 +11,10 @@ import { InlineError } from '~/common/components/InlineError'; import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon'; import { createDConversation, createDMessage, DMessage, useChatStore } from '~/common/state/store-chats'; -import type { ChatGptSharedChatSchema } from './server/import.chatgpt'; +import type { ChatGptSharedChatSchema } from '../server/import.chatgpt'; +import { loadAllConversationsFromJson } from '../trade.client'; + import { ImportedOutcome, ImportOutcomeModal } from './ImportOutcomeModal'; -import { loadAllConversationsFromJson } from './trade.client'; export type ImportConfig = { dir: 'import' }; diff --git a/src/apps/chat/trade/ImportOutcomeModal.tsx b/src/apps/chat/trade/import/ImportOutcomeModal.tsx similarity index 100% rename from src/apps/chat/trade/ImportOutcomeModal.tsx rename to src/apps/chat/trade/import/ImportOutcomeModal.tsx diff --git a/src/apps/chat/trade/server/share.server.ts b/src/apps/chat/trade/server/storage.server.ts similarity index 55% rename from src/apps/chat/trade/server/share.server.ts rename to src/apps/chat/trade/server/storage.server.ts index 1c12c0bd2d..25563d9ea7 100644 --- a/src/apps/chat/trade/server/share.server.ts +++ b/src/apps/chat/trade/server/storage.server.ts @@ -1,33 +1,38 @@ import { z } from 'zod'; -import { SharingDataType } from '@prisma/client'; +import { LinkStorageDataType, LinkStorageVisibility } from '@prisma/client'; import { db } from '~/server/db'; import { publicProcedure } from '~/server/api/trpc.server'; import { v4 as uuidv4 } from 'uuid'; +// configuration +const DEFAULT_EXPIRES_SECONDS = 60 * 60 * 24 * 30; // 30 days + + /// Zod schemas -const dataTypesSchema = z.enum([SharingDataType.CHAT_V1]); -const dataSchema = z.object({}).nonstrict(); +const dataTypesSchema = z.enum([LinkStorageDataType.CHAT_V1]); +const dataSchema = z.object({}).passthrough(); -const sharePutInputSchema = z.object({ +const storagePutInputSchema = z.object({ ownerId: z.string().optional(), dataType: dataTypesSchema, + dataTitle: z.string().optional(), dataObject: dataSchema, expiresSeconds: z.number().optional(), }); -export const sharePutOutputSchema = z.union([ +export const storagePutOutputSchema = z.union([ z.object({ type: z.literal('success'), - sharedId: z.string(), + objectId: z.string(), ownerId: z.string(), + createdAt: z.date(), expiresAt: z.date().nullable(), deletionKey: z.string(), - createdAt: z.date(), }), z.object({ type: z.literal('error'), @@ -35,16 +40,18 @@ export const sharePutOutputSchema = z.union([ }), ]); -const shareGetInputSchema = z.object({ - sharedId: z.string(), +const storageGetInputSchema = z.object({ + objectId: z.string(), + ownerId: z.string().optional(), }); -export const shareGetOutputSchema = z.union([ +export const storageGetOutputSchema = z.union([ z.object({ type: z.literal('success'), dataType: dataTypesSchema, + dataTitle: z.string().nullable(), dataObject: dataSchema, - sharedAt: z.date(), + storedAt: z.date(), expiresAt: z.date().nullable(), }), z.object({ @@ -53,12 +60,13 @@ export const shareGetOutputSchema = z.union([ }), ]); -const shareDeleteInputSchema = z.object({ - sharedId: z.string(), +const storageDeleteInputSchema = z.object({ + objectId: z.string(), + ownerId: z.string().optional(), deletionKey: z.string(), }); -export const shareDeleteOutputSchema = z.object({ +export const storageDeleteOutputSchema = z.object({ type: z.enum(['success', 'error']), error: z.string().optional(), }); @@ -67,21 +75,17 @@ export const shareDeleteOutputSchema = z.object({ /// tRPC procedures /** - * Writes dataObject to DB, returns sharedId / ownerId + * Writes dataObject to DB, returns ownerId, objectId, and deletionKey */ -export const sharePutProcedure = +export const storagePutProcedure = publicProcedure - .input(sharePutInputSchema) - .output(sharePutOutputSchema) - .mutation(async ({ input: { ownerId, dataType, dataObject, expiresSeconds } }) => { - - // expire in 30 days, if unspecified - if (!expiresSeconds) - expiresSeconds = 60 * 60 * 24 * 30; + .input(storagePutInputSchema) + .output(storagePutOutputSchema) + .mutation(async ({ input }) => { - const dataSizeEstimate = JSON.stringify(dataObject).length; + const { ownerId, dataType, dataTitle, dataObject, expiresSeconds } = input; - const { id: sharedId, ...rest } = await db.sharing.create({ + const { id: objectId, ...rest } = await db.linkStorage.create({ select: { id: true, ownerId: true, @@ -91,11 +95,14 @@ export const sharePutProcedure = }, data: { ownerId: ownerId || uuidv4(), - isPublic: true, + visibility: LinkStorageVisibility.UNLISTED, dataType, - dataSize: dataSizeEstimate, + dataTitle, + dataSize: JSON.stringify(dataObject).length, // data size estimate data: dataObject, - expiresAt: new Date(Date.now() + 1000 * expiresSeconds), + expiresAt: expiresSeconds === 0 + ? undefined // never expires + : new Date(Date.now() + 1000 * (expiresSeconds || DEFAULT_EXPIRES_SECONDS)), // default deletionKey: uuidv4(), isDeleted: false, }, @@ -103,7 +110,7 @@ export const sharePutProcedure = return { type: 'success', - sharedId, + objectId, ...rest, }; @@ -111,25 +118,26 @@ export const sharePutProcedure = /** - * Read a public object from DB, if it exists, and is not expired, and is not deleted + * Reads an object from DB, if it exists, and is not expired, and is not marked as deleted */ -export const shareGetProducedure = +export const storageGetProcedure = publicProcedure - .input(shareGetInputSchema) - .output(shareGetOutputSchema) - .query(async ({ input: { sharedId } }) => { + .input(storageGetInputSchema) + .output(storageGetOutputSchema) + .query(async ({ input: { objectId, ownerId } }) => { // read object - const result = await db.sharing.findUnique({ + const result = await db.linkStorage.findUnique({ select: { dataType: true, + dataTitle: true, data: true, createdAt: true, expiresAt: true, }, where: { - id: sharedId, - isPublic: true, + id: objectId, + ownerId: ownerId || undefined, isDeleted: false, OR: [ { expiresAt: null }, @@ -154,12 +162,12 @@ export const shareGetProducedure = // increment the read count // NOTE: fire-and-forget; we don't care about the result { - db.sharing.update({ + db.linkStorage.update({ select: { id: true, }, where: { - id: sharedId, + id: objectId, }, data: { readCount: { @@ -172,8 +180,9 @@ export const shareGetProducedure = return { type: 'success', dataType: result.dataType, + dataTitle: result.dataTitle, dataObject: result.data as any, - sharedAt: result.createdAt, + storedAt: result.createdAt, expiresAt: result.expiresAt, }; @@ -183,15 +192,16 @@ export const shareGetProducedure = /** * Mark a public object as deleted, if it exists, and is not expired, and is not deleted */ -export const shareDeleteProcedure = +export const storageMarkAsDeletedProcedure = publicProcedure - .input(shareDeleteInputSchema) - .output(shareDeleteOutputSchema) - .mutation(async ({ input: { sharedId, deletionKey } }) => { + .input(storageDeleteInputSchema) + .output(storageDeleteOutputSchema) + .mutation(async ({ input: { objectId, ownerId, deletionKey } }) => { - const result = await db.sharing.updateMany({ + const result = await db.linkStorage.updateMany({ where: { - id: sharedId, + id: objectId, + ownerId: ownerId || undefined, deletionKey, isDeleted: false, }, diff --git a/src/apps/chat/trade/server/trade.router.ts b/src/apps/chat/trade/server/trade.router.ts index e687823dfb..4422d40aef 100644 --- a/src/apps/chat/trade/server/trade.router.ts +++ b/src/apps/chat/trade/server/trade.router.ts @@ -6,12 +6,12 @@ import { fetchTextOrTRPCError } from '~/server/api/trpc.serverutils'; import { chatGptImportConversation, chatGptSharedChatSchema } from './import.chatgpt'; import { postToPasteGGOrThrow, publishToInputSchema, publishToOutputSchema } from './publish.pastegg'; -import { shareDeleteOutputSchema, shareDeleteProcedure, shareGetProducedure, sharePutOutputSchema, sharePutProcedure } from './share.server'; +import { storageDeleteOutputSchema, storageGetProcedure, storageMarkAsDeletedProcedure, storagePutOutputSchema, storagePutProcedure } from './storage.server'; -export type SharePutSchema = z.infer; +export type StoragePutSchema = z.infer; -export type ShareDeleteSchema = z.infer; +export type StorageDeleteSchema = z.infer; export type PublishedSchema = z.infer; @@ -34,19 +34,19 @@ export const tradeRouter = createTRPCRouter({ }), /** - * Experimental: 'Sharing functionality': server-side storage + * Write an object to storage, and return the ID, owner, and deletion key */ - sharePut: sharePutProcedure, + storagePut: storagePutProcedure, /** - * This function will read the shared data by ID, but only if not deleted or expired + * Read a stored object by ID (optional owner) */ - shareGet: shareGetProducedure, + storageGet: storageGetProcedure, /** - * This function will delete the shared data by ID, but only if not deleted or expired + * Delete a stored object by ID and deletion key */ - shareDelete: shareDeleteProcedure, + storageDelete: storageMarkAsDeletedProcedure, /** * Publish a text file (with title, content, name) to a sharing service diff --git a/src/apps/chat/trade/trade.client.ts b/src/apps/chat/trade/trade.client.ts index e1d70441a5..d276438405 100644 --- a/src/apps/chat/trade/trade.client.ts +++ b/src/apps/chat/trade/trade.client.ts @@ -7,7 +7,7 @@ import { DModelSource, useModelsStore } from '~/modules/llms/store-llms'; import { DConversation, DMessage, useChatStore } from '~/common/state/store-chats'; import { prettyBaseModel } from '~/common/util/modelUtils'; -import { ImportedOutcome } from './ImportOutcomeModal'; +import { ImportedOutcome } from './import/ImportOutcomeModal'; /** diff --git a/src/apps/shared/AppShared.tsx b/src/apps/link/AppChatLink.tsx similarity index 56% rename from src/apps/shared/AppShared.tsx rename to src/apps/link/AppChatLink.tsx index 22123fc1b1..c91c8f73f3 100644 --- a/src/apps/shared/AppShared.tsx +++ b/src/apps/link/AppChatLink.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import Head from 'next/head'; import { useQuery } from '@tanstack/react-query'; -import { useRouter } from 'next/router'; import { Box, Typography } from '@mui/joy'; @@ -11,8 +10,10 @@ import { Brand } from '~/common/brand'; import { InlineError } from '~/common/components/InlineError'; import { LogoProgress } from '~/common/components/LogoProgress'; import { apiAsyncNode } from '~/common/util/trpc.client'; +import { capitalizeFirstLetter } from '~/common/util/textUtils'; +import { conversationTitle } from '~/common/state/store-chats'; -import { ViewSharedConversation } from './ViewSharedConversation'; +import { ViewChatLink } from './ViewChatLink'; const Centerer = (props: { backgroundColor: string, children?: React.ReactNode }) => @@ -39,61 +40,56 @@ const ShowError = (props: { error: any }) => /** - * Fetches the shared conversation from the server + * Fetches the object using tRPC * Note: we don't have react-query for the Node functions, so we use the immediate API here, * and wrap it in a react-query hook */ -async function fetchSharedConversation(sharedId: string) { +async function fetchStoredChatV1(objectId: string) { // fetch - const result = await apiAsyncNode.trade.shareGet.query({ sharedId }); - - // validate + const result = await apiAsyncNode.trade.storageGet.query({ objectId }); if (result.type === 'error') throw result.error; - if (result.dataType !== 'CHAT_V1') - throw new Error('Unsupported data type: ' + result.dataType); + + // validate a CHAT_V1 + const { dataType, dataObject, storedAt, expiresAt } = result; + if (dataType !== 'CHAT_V1') + throw new Error('Unsupported data type: ' + dataType); // convert to DConversation - const restored = createConversationFromJsonV1(result.dataObject as any); + const restored = createConversationFromJsonV1(dataObject as any); if (!restored) throw new Error('Could not restore conversation'); - return { - conversation: restored, - sharedAt: result.sharedAt, - expiresAt: result.expiresAt, - }; + return { conversation: restored, storedAt, expiresAt }; } -export function AppShared() { - - // state - const { query } = useRouter(); - const sharedId = query.sharedId as string ?? ''; +export function AppChatLink(props: { linkId: string }) { // external state const { data, isError, error, isLoading } = useQuery({ - enabled: !!sharedId, - queryKey: ['app-shared-chat', sharedId], - queryFn: () => fetchSharedConversation(sharedId), + enabled: !!props.linkId, + queryKey: ['chat-link', props.linkId], + queryFn: () => fetchStoredChatV1(props.linkId), refetchOnMount: false, refetchOnWindowFocus: false, }); - if (isLoading) - return ; - - if (isError) - return ; - - if (!data?.conversation) - return ; + const pageTitle = (data?.conversation && conversationTitle(data.conversation)) || 'Chat Link'; return <> + - {Brand.Title.Common} - Shared Chat + {capitalizeFirstLetter(pageTitle)} ยท {Brand.Title.Base} ๐Ÿš€ - + + {isLoading + ? + : isError + ? + : !!data?.conversation + ? + : } + ; } \ No newline at end of file diff --git a/src/apps/shared/ViewSharedConversation.tsx b/src/apps/link/ViewChatLink.tsx similarity index 94% rename from src/apps/shared/ViewSharedConversation.tsx rename to src/apps/link/ViewChatLink.tsx index 4dd42939c2..1e726a178a 100644 --- a/src/apps/shared/ViewSharedConversation.tsx +++ b/src/apps/link/ViewChatLink.tsx @@ -7,10 +7,9 @@ import { Box, Button, Card, List, ListItem, MenuItem, Switch, Tooltip, Typograph import TelegramIcon from '@mui/icons-material/Telegram'; import { ChatMessage } from '../chat/components/message/ChatMessage'; -import { conversationTitle } from '../chat/components/applayout/ConversationItem'; import { Brand } from '~/common/brand'; -import { DConversation, useChatStore } from '~/common/state/store-chats'; +import { conversationTitle, DConversation, useChatStore } from '~/common/state/store-chats'; import { navigateToChat } from '~/common/routes'; import { useLayoutPluggable } from '~/common/layout/store-applayout'; import { useUIPreferencesStore } from '~/common/state/store-ui'; @@ -29,7 +28,7 @@ import { useUIPreferencesStore } from '~/common/state/store-ui'; }`; */ -function AppSharedMenuItems() { +function AppChatLinkMenuItems() { // external state const { @@ -73,7 +72,7 @@ function AppSharedMenuItems() { ; } -export function ViewSharedConversation(props: { conversation: DConversation, sharedAt: Date, expiresAt: Date | null }) { +export function ViewChatLink(props: { conversation: DConversation, storedAt: Date, expiresAt: Date | null }) { // state const [cloning, setCloning] = React.useState(false); @@ -94,7 +93,7 @@ export function ViewSharedConversation(props: { conversation: DConversation, sha // pluggable UI const menuItems = React.useMemo(() => - , + , [], ); @@ -152,10 +151,10 @@ export function ViewSharedConversation(props: { conversation: DConversation, sha // animation: `${cssMagicSwapKeyframes} 0.4s cubic-bezier(0.22, 1, 0.36, 1)`, }}> }> - {conversationTitle(props.conversation)} + {conversationTitle(props.conversation, 'Chat')} - Uploaded + Uploaded {!!props.expiresAt && <>, expires }. diff --git a/src/common/routes.ts b/src/common/routes.ts index 9cf6b2b6c7..4204e1053c 100644 --- a/src/common/routes.ts +++ b/src/common/routes.ts @@ -7,10 +7,10 @@ import { NextRouter } from 'next/router'; const APP_CHAT = '/'; -const APP_SHARING = '/shared/:sharedId'; +const APP_LINK_CHAT = '/link/chat/:linkId'; -export const getSharingRelativePath = (sharedId: string) => APP_SHARING.replace(':sharedId', sharedId); +export const getChatLinkRelativePath = (chatLinkId: string) => APP_LINK_CHAT.replace(':linkId', chatLinkId); export const navigateToChat = async (next: NextRouter['push'] | NextRouter['replace']) => next(APP_CHAT).then(() => null); \ No newline at end of file diff --git a/src/common/state/store-ui.ts b/src/common/state/store-ui.ts index 8e518f08c1..3d62803772 100644 --- a/src/common/state/store-ui.ts +++ b/src/common/state/store-ui.ts @@ -71,7 +71,7 @@ const useUICountersStore = create()( }), ); -type UiCounterKey = 'export-share' | 'share-web'; +type UiCounterKey = 'export-share' | 'share-chat-link'; export function useUICounter(key: UiCounterKey) { const value = useUICountersStore(state => state.actionCounters[key] || 0);