Skip to content

Commit

Permalink
Trade: update deletion key #356
Browse files Browse the repository at this point in the history
  • Loading branch information
enricoros committed Jan 22, 2024
1 parent db3a435 commit f007313
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 7 deletions.
78 changes: 75 additions & 3 deletions src/modules/trade/chatlink/ChatLinkDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as React from 'react';
import TimeAgo from 'react-timeago';

import { Button, Card, Input, Stack, Tooltip, Typography } from '@mui/joy';
import { Box, Button, Card, IconButton, Input, Stack, Tooltip, Typography } from '@mui/joy';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import DoneIcon from '@mui/icons-material/Done';
import EditIcon from '@mui/icons-material/Edit';
import IosShareIcon from '@mui/icons-material/IosShare';
import LaunchIcon from '@mui/icons-material/Launch';
import LinkIcon from '@mui/icons-material/Link';
Expand All @@ -12,6 +14,7 @@ import { Brand } from '~/common/app.config';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import { GoodModal } from '~/common/components/GoodModal';
import { InlineError } from '~/common/components/InlineError';
import { InlineTextarea } from '~/common/components/InlineTextarea';
import { Link } from '~/common/components/Link';
import { apiAsyncNode } from '~/common/util/trpc.client';
import { copyToClipboard } from '~/common/util/clipboardUtils';
Expand All @@ -23,12 +26,18 @@ import type { StorageDeleteSchema, StoragePutSchema } from '../server/link';
import { forgetChatLinkItem } from './store-chatlink';


export function ChatLinkDetails(props: { onClose: () => void, storageItem: StoragePutSchema, open: boolean }) {
export function ChatLinkDetails(props: {
open: boolean,
onClose: () => void,
storageItem: StoragePutSchema,
onChangeDeletionKey: (deletionKey: string) => void,
}) {

// state
const [opened, setOpened] = React.useState(false);
const [copied, setCopied] = React.useState(false);
const [native, setNative] = React.useState(false);
const [isEditingDeletionKey, setIsEditingDeletionKey] = React.useState(false);
const [confirmDeletion, setConfirmDeletion] = React.useState(false);
const [deletionResponse, setDeletionResponse] = React.useState<StorageDeleteSchema | null>(null);

Expand All @@ -47,6 +56,26 @@ export function ChatLinkDetails(props: { onClose: () => void, storageItem: Stora
const fullUrl = getOriginUrl() + relativeUrl;


// Deletion Key Edit

const handleKeyEditBegin = () => setIsEditingDeletionKey(true);

const handleKeyEditCancel = () => setIsEditingDeletionKey(false);

const handleKeyEditChange = (text: string) => {
if (text) {
setIsEditingDeletionKey(false);
props.onChangeDeletionKey(text.trim());
}
};

// Deletion Key Copy

const handleKeyCopy = () => {
copyToClipboard(deletionKey, 'Link Deletion Key');
};


const onOpen = () => setOpened(true);

const onCopy = () => {
Expand Down Expand Up @@ -138,7 +167,50 @@ export function ChatLinkDetails(props: { onClose: () => void, storageItem: Stora
Deletion Key
</Typography>

<Input readOnly variant='plain' value={deletionKey} sx={{ flexGrow: 1 }} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{isEditingDeletionKey ? (
<InlineTextarea
invertedColors
initialText={deletionKey}
onEdit={handleKeyEditChange}
onCancel={handleKeyEditCancel}
sx={{
flexGrow: 1,
ml: -1.5, mr: -0.5,
}}
/>
) : (
<Input
readOnly
variant='plain'
value={deletionKey}
endDecorator={
<Box sx={{ display: 'flex', gap: 2 }}>
<Tooltip title='Edit Deletion Key'>
<IconButton
variant='soft'
color='primary'
disabled={isEditingDeletionKey}
onClick={handleKeyEditBegin}
>
<EditIcon />
</IconButton>
</Tooltip>
<IconButton
variant='soft'
color='primary'
disabled={isEditingDeletionKey}
onClick={handleKeyCopy}
>
<ContentCopyIcon />
</IconButton>
</Box>
}
sx={{ flexGrow: 1 }}
/>
)}
</Box>


<Typography level='body-sm'>
IMPORTANT - <b>keep this key safe</b>, you will need it if you decide to delete the link at a later time,
Expand Down
51 changes: 48 additions & 3 deletions src/modules/trade/chatlink/ChatLinkExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import IosShareIcon from '@mui/icons-material/IosShare';
import { Brand } from '~/common/app.config';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import { Link } from '~/common/components/Link';
import { addSnackbar } from '~/common/components/useSnackbarsStore';
import { apiAsyncNode } from '~/common/util/trpc.client';
import { conversationTitle, DConversationId, getConversation } from '~/common/state/store-chats';
import { useUICounter } from '~/common/state/store-ui';

import type { StoragePutSchema } from '../server/link';
import type { StoragePutSchema, StorageUpdateDeletionKeySchema } from '../server/link';
import { ChatLinkDetails } from './ChatLinkDetails';
import { conversationToJsonV1 } from '../trade.client';
import { rememberChatLinkItem, useLinkStorageOwnerId } from './store-chatlink';
import { rememberChatLinkItem, updateChatLinkDeletionKey, useLinkStorageOwnerId } from './store-chatlink';


export function ChatLinkExport(props: {
Expand Down Expand Up @@ -70,6 +71,45 @@ export function ChatLinkExport(props: {
setIsUploading(false);
};

const handleChangeDeletionKey = async (newKey: string) => {
if (!linkPutResult || linkPutResult.type !== 'success') return;
const { objectId, deletionKey: formerKey } = linkPutResult;
if (!objectId || !formerKey || !newKey || formerKey === newKey) return;

// update it in the Storage
try {
const response: StorageUpdateDeletionKeySchema = await apiAsyncNode.trade.storageUpdateDeletionKey.mutate({
objectId,
formerKey,
newKey,
});
if (response.type === 'error') {
addSnackbar({
key: 'chatlink-deletion-key-update-error',
type: 'issue',
message: `Failed to update deletion key: ${response.error}`,
});
return;
}
} catch (error: any) {
addSnackbar({
key: 'chatlink-deletion-key-update-exception',
type: 'issue',
message: `Failed to update deletion key: ${error?.message ?? error?.toString() ?? 'unknown error'}`,
});
return;
}

// update it in the local storage
updateChatLinkDeletionKey(objectId, newKey);

// update it in the status
setLinkPutResult({
...linkPutResult,
deletionKey: newKey,
});
};

const handleCloseDetails = () => setLinkPutResult(null);


Expand Down Expand Up @@ -107,7 +147,12 @@ export function ChatLinkExport(props: {

{/* [chat link] response */}
{!!linkPutResult && (
<ChatLinkDetails open storageItem={linkPutResult} onClose={handleCloseDetails} />
<ChatLinkDetails
open
storageItem={linkPutResult}
onClose={handleCloseDetails}
onChangeDeletionKey={handleChangeDeletionKey}
/>
)}

</>;
Expand Down
5 changes: 5 additions & 0 deletions src/modules/trade/chatlink/store-chatlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface ModuleTradeStore {
chatLinkItems: ChatLinkItem[];
rememberChatLinkItem: (chatTitle: string | undefined, objectId: string, createdAt: Date, expiresAt: Date | null, deletionKey: string) => void;
forgetChatLinkItem: (objectId: string) => void;
updateChatLinkDeletionKey: (objectId: string, deletionKey: string) => void;

// ID assigned by the server upon first PUT
linkStorageOwnerId: string | undefined;
Expand All @@ -35,6 +36,9 @@ const useTradeStore = create<ModuleTradeStore>()(
forgetChatLinkItem: (objectId: string) => set(state => ({
chatLinkItems: state.chatLinkItems.filter(item => item.objectId !== objectId),
})),
updateChatLinkDeletionKey: (objectId: string, deletionKey: string) => set(state => ({
chatLinkItems: state.chatLinkItems.map(item => item.objectId === objectId ? { ...item, deletionKey } : item),
})),

linkStorageOwnerId: undefined,
setLinkStorageOwnerId: (linkStorageOwnerId: string) => set({ linkStorageOwnerId }),
Expand All @@ -59,3 +63,4 @@ export const useLinkStorageOwnerId = () =>
}), shallow);
export const rememberChatLinkItem = useTradeStore.getState().rememberChatLinkItem;
export const forgetChatLinkItem = useTradeStore.getState().forgetChatLinkItem;
export const updateChatLinkDeletionKey = useTradeStore.getState().updateChatLinkDeletionKey;
43 changes: 43 additions & 0 deletions src/modules/trade/server/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,22 @@ export const storageDeleteOutputSchema = z.object({
error: z.string().optional(),
});

const storageUpdateDeletionKeyInputSchema = z.object({
objectId: z.string(),
ownerId: z.string().optional(),
formerKey: z.string(),
newKey: z.string(),
});

export const storageUpdateDeletionKeyOutputSchema = z.object({
type: z.enum(['success', 'error']),
error: z.string().optional(),
});


export type StoragePutSchema = z.infer<typeof storagePutOutputSchema>;
export type StorageDeleteSchema = z.infer<typeof storageDeleteOutputSchema>;
export type StorageUpdateDeletionKeySchema = z.infer<typeof storageUpdateDeletionKeyOutputSchema>;


/// tRPC procedures
Expand Down Expand Up @@ -222,3 +235,33 @@ export const storageMarkAsDeletedProcedure =
error: success ? undefined : 'Not found',
};
});


/**
* Update the deletion Key of a public object by ID and deletion key
*/
export const storageUpdateDeletionKeyProcedure =
publicProcedure
.input(storageUpdateDeletionKeyInputSchema)
.output(storageUpdateDeletionKeyOutputSchema)
.mutation(async ({ input: { objectId, ownerId, formerKey, newKey } }) => {

const result = await db.linkStorage.updateMany({
where: {
id: objectId,
ownerId: ownerId || undefined,
deletionKey: formerKey,
isDeleted: false,
},
data: {
deletionKey: newKey,
},
});

const success = result.count === 1;

return {
type: success ? 'success' : 'error',
error: success ? undefined : 'Not found',
};
});
7 changes: 6 additions & 1 deletion src/modules/trade/server/trade.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fetchTextOrTRPCError } from '~/server/api/trpc.serverutils';

import { chatGptParseConversation, chatGptSharedChatSchema } from './chatgpt';
import { postToPasteGGOrThrow, publishToInputSchema, publishToOutputSchema } from './pastegg';
import { storageGetProcedure, storageMarkAsDeletedProcedure, storagePutProcedure } from './link';
import { storageGetProcedure, storageMarkAsDeletedProcedure, storagePutProcedure, storageUpdateDeletionKeyProcedure } from './link';


export const importChatGptShareInputSchema = z.union([
Expand Down Expand Up @@ -65,6 +65,11 @@ export const tradeRouter = createTRPCRouter({
*/
storageDelete: storageMarkAsDeletedProcedure,

/**
* Update the deletion Key of a stored object by ID and deletion key
*/
storageUpdateDeletionKey: storageUpdateDeletionKeyProcedure,

/**
* Publish a text file (with title, content, name) to a sharing service
* For now only 'paste.gg' is supported
Expand Down

0 comments on commit f007313

Please sign in to comment.