Skip to content

Commit f007313

Browse files
committed
Trade: update deletion key #356
1 parent db3a435 commit f007313

File tree

5 files changed

+177
-7
lines changed

5 files changed

+177
-7
lines changed

src/modules/trade/chatlink/ChatLinkDetails.tsx

+75-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as React from 'react';
22
import TimeAgo from 'react-timeago';
33

4-
import { Button, Card, Input, Stack, Tooltip, Typography } from '@mui/joy';
4+
import { Box, Button, Card, IconButton, Input, Stack, Tooltip, Typography } from '@mui/joy';
5+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
56
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
67
import DoneIcon from '@mui/icons-material/Done';
8+
import EditIcon from '@mui/icons-material/Edit';
79
import IosShareIcon from '@mui/icons-material/IosShare';
810
import LaunchIcon from '@mui/icons-material/Launch';
911
import LinkIcon from '@mui/icons-material/Link';
@@ -12,6 +14,7 @@ import { Brand } from '~/common/app.config';
1214
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
1315
import { GoodModal } from '~/common/components/GoodModal';
1416
import { InlineError } from '~/common/components/InlineError';
17+
import { InlineTextarea } from '~/common/components/InlineTextarea';
1518
import { Link } from '~/common/components/Link';
1619
import { apiAsyncNode } from '~/common/util/trpc.client';
1720
import { copyToClipboard } from '~/common/util/clipboardUtils';
@@ -23,12 +26,18 @@ import type { StorageDeleteSchema, StoragePutSchema } from '../server/link';
2326
import { forgetChatLinkItem } from './store-chatlink';
2427

2528

26-
export function ChatLinkDetails(props: { onClose: () => void, storageItem: StoragePutSchema, open: boolean }) {
29+
export function ChatLinkDetails(props: {
30+
open: boolean,
31+
onClose: () => void,
32+
storageItem: StoragePutSchema,
33+
onChangeDeletionKey: (deletionKey: string) => void,
34+
}) {
2735

2836
// state
2937
const [opened, setOpened] = React.useState(false);
3038
const [copied, setCopied] = React.useState(false);
3139
const [native, setNative] = React.useState(false);
40+
const [isEditingDeletionKey, setIsEditingDeletionKey] = React.useState(false);
3241
const [confirmDeletion, setConfirmDeletion] = React.useState(false);
3342
const [deletionResponse, setDeletionResponse] = React.useState<StorageDeleteSchema | null>(null);
3443

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

4958

59+
// Deletion Key Edit
60+
61+
const handleKeyEditBegin = () => setIsEditingDeletionKey(true);
62+
63+
const handleKeyEditCancel = () => setIsEditingDeletionKey(false);
64+
65+
const handleKeyEditChange = (text: string) => {
66+
if (text) {
67+
setIsEditingDeletionKey(false);
68+
props.onChangeDeletionKey(text.trim());
69+
}
70+
};
71+
72+
// Deletion Key Copy
73+
74+
const handleKeyCopy = () => {
75+
copyToClipboard(deletionKey, 'Link Deletion Key');
76+
};
77+
78+
5079
const onOpen = () => setOpened(true);
5180

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

141-
<Input readOnly variant='plain' value={deletionKey} sx={{ flexGrow: 1 }} />
170+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
171+
{isEditingDeletionKey ? (
172+
<InlineTextarea
173+
invertedColors
174+
initialText={deletionKey}
175+
onEdit={handleKeyEditChange}
176+
onCancel={handleKeyEditCancel}
177+
sx={{
178+
flexGrow: 1,
179+
ml: -1.5, mr: -0.5,
180+
}}
181+
/>
182+
) : (
183+
<Input
184+
readOnly
185+
variant='plain'
186+
value={deletionKey}
187+
endDecorator={
188+
<Box sx={{ display: 'flex', gap: 2 }}>
189+
<Tooltip title='Edit Deletion Key'>
190+
<IconButton
191+
variant='soft'
192+
color='primary'
193+
disabled={isEditingDeletionKey}
194+
onClick={handleKeyEditBegin}
195+
>
196+
<EditIcon />
197+
</IconButton>
198+
</Tooltip>
199+
<IconButton
200+
variant='soft'
201+
color='primary'
202+
disabled={isEditingDeletionKey}
203+
onClick={handleKeyCopy}
204+
>
205+
<ContentCopyIcon />
206+
</IconButton>
207+
</Box>
208+
}
209+
sx={{ flexGrow: 1 }}
210+
/>
211+
)}
212+
</Box>
213+
142214

143215
<Typography level='body-sm'>
144216
IMPORTANT - <b>keep this key safe</b>, you will need it if you decide to delete the link at a later time,

src/modules/trade/chatlink/ChatLinkExport.tsx

+48-3
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import IosShareIcon from '@mui/icons-material/IosShare';
77
import { Brand } from '~/common/app.config';
88
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
99
import { Link } from '~/common/components/Link';
10+
import { addSnackbar } from '~/common/components/useSnackbarsStore';
1011
import { apiAsyncNode } from '~/common/util/trpc.client';
1112
import { conversationTitle, DConversationId, getConversation } from '~/common/state/store-chats';
1213
import { useUICounter } from '~/common/state/store-ui';
1314

14-
import type { StoragePutSchema } from '../server/link';
15+
import type { StoragePutSchema, StorageUpdateDeletionKeySchema } from '../server/link';
1516
import { ChatLinkDetails } from './ChatLinkDetails';
1617
import { conversationToJsonV1 } from '../trade.client';
17-
import { rememberChatLinkItem, useLinkStorageOwnerId } from './store-chatlink';
18+
import { rememberChatLinkItem, updateChatLinkDeletionKey, useLinkStorageOwnerId } from './store-chatlink';
1819

1920

2021
export function ChatLinkExport(props: {
@@ -70,6 +71,45 @@ export function ChatLinkExport(props: {
7071
setIsUploading(false);
7172
};
7273

74+
const handleChangeDeletionKey = async (newKey: string) => {
75+
if (!linkPutResult || linkPutResult.type !== 'success') return;
76+
const { objectId, deletionKey: formerKey } = linkPutResult;
77+
if (!objectId || !formerKey || !newKey || formerKey === newKey) return;
78+
79+
// update it in the Storage
80+
try {
81+
const response: StorageUpdateDeletionKeySchema = await apiAsyncNode.trade.storageUpdateDeletionKey.mutate({
82+
objectId,
83+
formerKey,
84+
newKey,
85+
});
86+
if (response.type === 'error') {
87+
addSnackbar({
88+
key: 'chatlink-deletion-key-update-error',
89+
type: 'issue',
90+
message: `Failed to update deletion key: ${response.error}`,
91+
});
92+
return;
93+
}
94+
} catch (error: any) {
95+
addSnackbar({
96+
key: 'chatlink-deletion-key-update-exception',
97+
type: 'issue',
98+
message: `Failed to update deletion key: ${error?.message ?? error?.toString() ?? 'unknown error'}`,
99+
});
100+
return;
101+
}
102+
103+
// update it in the local storage
104+
updateChatLinkDeletionKey(objectId, newKey);
105+
106+
// update it in the status
107+
setLinkPutResult({
108+
...linkPutResult,
109+
deletionKey: newKey,
110+
});
111+
};
112+
73113
const handleCloseDetails = () => setLinkPutResult(null);
74114

75115

@@ -107,7 +147,12 @@ export function ChatLinkExport(props: {
107147

108148
{/* [chat link] response */}
109149
{!!linkPutResult && (
110-
<ChatLinkDetails open storageItem={linkPutResult} onClose={handleCloseDetails} />
150+
<ChatLinkDetails
151+
open
152+
storageItem={linkPutResult}
153+
onClose={handleCloseDetails}
154+
onChangeDeletionKey={handleChangeDeletionKey}
155+
/>
111156
)}
112157

113158
</>;

src/modules/trade/chatlink/store-chatlink.ts

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface ModuleTradeStore {
1717
chatLinkItems: ChatLinkItem[];
1818
rememberChatLinkItem: (chatTitle: string | undefined, objectId: string, createdAt: Date, expiresAt: Date | null, deletionKey: string) => void;
1919
forgetChatLinkItem: (objectId: string) => void;
20+
updateChatLinkDeletionKey: (objectId: string, deletionKey: string) => void;
2021

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

3943
linkStorageOwnerId: undefined,
4044
setLinkStorageOwnerId: (linkStorageOwnerId: string) => set({ linkStorageOwnerId }),
@@ -59,3 +63,4 @@ export const useLinkStorageOwnerId = () =>
5963
}), shallow);
6064
export const rememberChatLinkItem = useTradeStore.getState().rememberChatLinkItem;
6165
export const forgetChatLinkItem = useTradeStore.getState().forgetChatLinkItem;
66+
export const updateChatLinkDeletionKey = useTradeStore.getState().updateChatLinkDeletionKey;

src/modules/trade/server/link.ts

+43
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,22 @@ export const storageDeleteOutputSchema = z.object({
7171
error: z.string().optional(),
7272
});
7373

74+
const storageUpdateDeletionKeyInputSchema = z.object({
75+
objectId: z.string(),
76+
ownerId: z.string().optional(),
77+
formerKey: z.string(),
78+
newKey: z.string(),
79+
});
80+
81+
export const storageUpdateDeletionKeyOutputSchema = z.object({
82+
type: z.enum(['success', 'error']),
83+
error: z.string().optional(),
84+
});
85+
7486

7587
export type StoragePutSchema = z.infer<typeof storagePutOutputSchema>;
7688
export type StorageDeleteSchema = z.infer<typeof storageDeleteOutputSchema>;
89+
export type StorageUpdateDeletionKeySchema = z.infer<typeof storageUpdateDeletionKeyOutputSchema>;
7790

7891

7992
/// tRPC procedures
@@ -222,3 +235,33 @@ export const storageMarkAsDeletedProcedure =
222235
error: success ? undefined : 'Not found',
223236
};
224237
});
238+
239+
240+
/**
241+
* Update the deletion Key of a public object by ID and deletion key
242+
*/
243+
export const storageUpdateDeletionKeyProcedure =
244+
publicProcedure
245+
.input(storageUpdateDeletionKeyInputSchema)
246+
.output(storageUpdateDeletionKeyOutputSchema)
247+
.mutation(async ({ input: { objectId, ownerId, formerKey, newKey } }) => {
248+
249+
const result = await db.linkStorage.updateMany({
250+
where: {
251+
id: objectId,
252+
ownerId: ownerId || undefined,
253+
deletionKey: formerKey,
254+
isDeleted: false,
255+
},
256+
data: {
257+
deletionKey: newKey,
258+
},
259+
});
260+
261+
const success = result.count === 1;
262+
263+
return {
264+
type: success ? 'success' : 'error',
265+
error: success ? undefined : 'Not found',
266+
};
267+
});

src/modules/trade/server/trade.router.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { fetchTextOrTRPCError } from '~/server/api/trpc.serverutils';
66

77
import { chatGptParseConversation, chatGptSharedChatSchema } from './chatgpt';
88
import { postToPasteGGOrThrow, publishToInputSchema, publishToOutputSchema } from './pastegg';
9-
import { storageGetProcedure, storageMarkAsDeletedProcedure, storagePutProcedure } from './link';
9+
import { storageGetProcedure, storageMarkAsDeletedProcedure, storagePutProcedure, storageUpdateDeletionKeyProcedure } from './link';
1010

1111

1212
export const importChatGptShareInputSchema = z.union([
@@ -65,6 +65,11 @@ export const tradeRouter = createTRPCRouter({
6565
*/
6666
storageDelete: storageMarkAsDeletedProcedure,
6767

68+
/**
69+
* Update the deletion Key of a stored object by ID and deletion key
70+
*/
71+
storageUpdateDeletionKey: storageUpdateDeletionKeyProcedure,
72+
6873
/**
6974
* Publish a text file (with title, content, name) to a sharing service
7075
* For now only 'paste.gg' is supported

0 commit comments

Comments
 (0)