add sharing manage feature

This commit is contained in:
Zhang Minghan 2023-10-23 16:04:40 +08:00
parent 248e2043a6
commit 3bc8b892fb
19 changed files with 594 additions and 50 deletions

View File

@ -10,6 +10,7 @@ import {
Boxes, Boxes,
CalendarPlus, CalendarPlus,
Cloud, Cloud,
ListStart,
Menu, Menu,
Plug, Plug,
} from "lucide-react"; } from "lucide-react";
@ -39,9 +40,11 @@ import { openDialog as openQuotaDialog, quotaSelector } from "./store/quota.ts";
import { openDialog as openPackageDialog } from "./store/package.ts"; import { openDialog as openPackageDialog } from "./store/package.ts";
import { openDialog as openSub } from "./store/subscription.ts"; import { openDialog as openSub } from "./store/subscription.ts";
import { openDialog as openApiDialog } from "./store/api.ts"; import { openDialog as openApiDialog } from "./store/api.ts";
import { openDialog as openSharingDialog } from "./store/sharing.ts";
import Package from "./routes/Package.tsx"; import Package from "./routes/Package.tsx";
import Subscription from "./routes/Subscription.tsx"; import Subscription from "./routes/Subscription.tsx";
import ApiKey from "./routes/ApiKey.tsx"; import ApiKey from "./routes/ApiKey.tsx";
import ShareManagement from "./routes/ShareManagement.tsx";
function Settings() { function Settings() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -78,6 +81,10 @@ function Settings() {
<Boxes className={`h-4 w-4 mr-1`} /> <Boxes className={`h-4 w-4 mr-1`} />
{t("pkg.title")} {t("pkg.title")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => dispatch(openSharingDialog())}>
<ListStart className={`h-4 w-4 mr-1`} />
{t("share.manage")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => dispatch(openApiDialog())}> <DropdownMenuItem onClick={() => dispatch(openApiDialog())}>
<Plug className={`h-4 w-4 mr-1`} /> <Plug className={`h-4 w-4 mr-1`} />
{t("api.title")} {t("api.title")}
@ -149,6 +156,7 @@ function App() {
<ApiKey /> <ApiKey />
<Package /> <Package />
<Subscription /> <Subscription />
<ShareManagement />
</Provider> </Provider>
); );
} }

View File

@ -20,7 +20,7 @@
--secondary: 210 40% 96.1%; --secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 222.2 47.4% 11.2%;
--muted: 240 4.8% 95.9%; --muted: 60 26% 92%;
--muted-foreground: 240 3.8% 46.1%; --muted-foreground: 240 3.8% 46.1%;
--accent: 37 26% 90%; --accent: 37 26% 90%;

View File

@ -0,0 +1,11 @@
.share-table {
max-height: 60vh;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
padding: 0 0.5rem;
&::-webkit-scrollbar {
width: 0.5rem;
}
}

View File

@ -1,11 +1,11 @@
import {useEffect, useRef, useState} from "react"; import { useEffect, useRef, useState } from "react";
import {Message} from "../../conversation/types.ts"; import { Message } from "../../conversation/types.ts";
import {useSelector} from "react-redux"; import { useSelector } from "react-redux";
import {selectCurrent, selectMessages} from "../../store/chat.ts"; import { selectCurrent, selectMessages } from "../../store/chat.ts";
import {Button} from "../ui/button.tsx"; import { Button } from "../ui/button.tsx";
import {ChevronDown} from "lucide-react"; import { ChevronDown } from "lucide-react";
import MessageSegment from "../Message.tsx"; import MessageSegment from "../Message.tsx";
import {connectionEvent} from "../../events/connection.ts"; import { connectionEvent } from "../../events/connection.ts";
function ChatInterface() { function ChatInterface() {
const ref = useRef(null); const ref = useRef(null);

View File

@ -1,18 +1,28 @@
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import React, {useEffect, useRef, useState} from "react"; import React, { useEffect, useRef, useState } from "react";
import FileProvider, {FileObject} from "../FileProvider.tsx"; import FileProvider, { FileObject } from "../FileProvider.tsx";
import {useDispatch, useSelector} from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import {selectAuthenticated, selectInit} from "../../store/auth.ts"; import { selectAuthenticated, selectInit } from "../../store/auth.ts";
import {selectMessages, selectModel, selectWeb, setWeb} from "../../store/chat.ts"; import {
import {manager} from "../../conversation/manager.ts"; selectMessages,
import {formatMessage} from "../../utils.ts"; selectModel,
selectWeb,
setWeb,
} from "../../store/chat.ts";
import { manager } from "../../conversation/manager.ts";
import { formatMessage } from "../../utils.ts";
import ChatInterface from "./ChatInterface.tsx"; import ChatInterface from "./ChatInterface.tsx";
import {Button} from "../ui/button.tsx"; import { Button } from "../ui/button.tsx";
import router from "../../router.ts"; import router from "../../router.ts";
import {BookMarked, ChevronRight, FolderKanban, Globe} from "lucide-react"; import { BookMarked, ChevronRight, FolderKanban, Globe } from "lucide-react";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "../ui/tooltip.tsx"; import {
import {Toggle} from "../ui/toggle.tsx"; Tooltip,
import {Input} from "../ui/input.tsx"; TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip.tsx";
import { Toggle } from "../ui/toggle.tsx";
import { Input } from "../ui/input.tsx";
import EditorProvider from "../EditorProvider.tsx"; import EditorProvider from "../EditorProvider.tsx";
import ModelSelector from "./ModelSelector.tsx"; import ModelSelector from "./ModelSelector.tsx";

View File

@ -1,27 +1,41 @@
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {useDispatch, useSelector} from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import {RootState} from "../../store"; import { RootState } from "../../store";
import {selectAuthenticated} from "../../store/auth.ts"; import { selectAuthenticated } from "../../store/auth.ts";
import {selectCurrent, selectHistory} from "../../store/chat.ts"; import { selectCurrent, selectHistory } from "../../store/chat.ts";
import {useRef, useState} from "react"; import { useRef, useState } from "react";
import {ConversationInstance} from "../../conversation/types.ts"; import { ConversationInstance } from "../../conversation/types.ts";
import {useToast} from "../ui/use-toast.ts"; import { useToast } from "../ui/use-toast.ts";
import {copyClipboard, extractMessage, filterMessage, mobile, useAnimation, useEffectAsync} from "../../utils.ts"; import {
import {deleteConversation, toggleConversation, updateConversationList} from "../../conversation/history.ts"; copyClipboard,
import {Button} from "../ui/button.tsx"; extractMessage,
import {setMenu} from "../../store/menu.ts"; filterMessage,
import {Copy, LogIn, Plus, RotateCw} from "lucide-react"; mobile,
useAnimation,
useEffectAsync,
} from "../../utils.ts";
import {
deleteConversation,
toggleConversation,
updateConversationList,
} from "../../conversation/history.ts";
import { Button } from "../ui/button.tsx";
import { setMenu } from "../../store/menu.ts";
import { Copy, LogIn, Plus, RotateCw } from "lucide-react";
import ConversationSegment from "./ConversationSegment.tsx"; import ConversationSegment from "./ConversationSegment.tsx";
import { import {
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent, AlertDialogContent,
AlertDialogDescription, AlertDialogFooter, AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle AlertDialogTitle,
} from "../ui/alert-dialog.tsx"; } from "../ui/alert-dialog.tsx";
import {shareConversation} from "../../conversation/sharing.ts"; import {getSharedLink, shareConversation} from "../../conversation/sharing.ts";
import {Input} from "../ui/input.tsx"; import { Input } from "../ui/input.tsx";
import {login} from "../../conf.ts"; import { login } from "../../conf.ts";
function SideBar() { function SideBar() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -185,7 +199,7 @@ function SideBar() {
operateConversation?.target?.id || -1, operateConversation?.target?.id || -1,
); );
if (resp.status) if (resp.status)
setShared(`${location.origin}/share/${resp.data}`); setShared(getSharedLink(resp.data));
else else
toast({ toast({
title: t("share.failed"), title: t("share.failed"),

View File

@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "./lib/utils.ts"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("bg-primary font-medium text-primary-foreground", className)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -1,7 +1,7 @@
import axios from "axios"; import axios from "axios";
import { Model } from "./conversation/types.ts"; import { Model } from "./conversation/types.ts";
export const version = "3.4.6"; export const version = "3.5.0";
export const deploy: boolean = true; export const deploy: boolean = true;
export let rest_api: string = "http://localhost:8094"; export let rest_api: string = "http://localhost:8094";
export let ws_api: string = "ws://localhost:8094"; export let ws_api: string = "ws://localhost:8094";

View File

@ -7,6 +7,13 @@ export type SharingForm = {
data: string; data: string;
}; };
export type SharingPreviewForm = {
name: string;
conversation_id: number;
hash: string;
time: string;
};
export type ViewData = { export type ViewData = {
name: string; name: string;
username: string; username: string;
@ -20,6 +27,17 @@ export type ViewForm = {
data: ViewData | null; data: ViewData | null;
}; };
export type ListSharingResponse = {
status: boolean;
message: string;
data?: SharingPreviewForm[];
};
export type DeleteSharingResponse = {
status: boolean;
message: string;
};
export async function shareConversation( export async function shareConversation(
id: number, id: number,
refs: number[] = [-1], refs: number[] = [-1],
@ -44,3 +62,33 @@ export async function viewConversation(hash: string): Promise<ViewForm> {
}; };
} }
} }
export async function listSharing(): Promise<ListSharingResponse> {
try {
const resp = await axios.get("/conversation/share/list");
return resp.data as ListSharingResponse;
} catch (e) {
return {
status: false,
message: (e as Error).message,
};
}
}
export async function deleteSharing(
hash: string,
): Promise<DeleteSharingResponse> {
try {
const resp = await axios.get(`/conversation/share/delete?hash=${hash}`);
return resp.data as DeleteSharingResponse;
} catch (e) {
return {
status: false,
message: (e as Error).message,
};
}
}
export function getSharedLink(hash: string): string {
return `${location.origin}/share/${hash}`;
}

View File

@ -186,10 +186,15 @@ const resources = {
"not-found": "Conversation not found", "not-found": "Conversation not found",
"not-found-description": "not-found-description":
"Conversation not found, please check if the link is correct or the conversation has been deleted", "Conversation not found, please check if the link is correct or the conversation has been deleted",
manage: "Manage Share",
"sync-error": "Sync Error",
name: "Conversation Title",
time: "Time",
action: "Action",
}, },
docs: { docs: {
title: "Open Docs", title: "Open Docs",
} },
}, },
}, },
cn: { cn: {
@ -362,10 +367,15 @@ const resources = {
"not-found": "对话未找到", "not-found": "对话未找到",
"not-found-description": "not-found-description":
"对话未找到,请检查链接是否正确或对话是否已被删除", "对话未找到,请检查链接是否正确或对话是否已被删除",
manage: "分享管理",
"sync-error": "同步失败",
name: "对话标题",
time: "时间",
action: "操作",
}, },
docs: { docs: {
title: "开放文档", title: "开放文档",
} },
}, },
}, },
ru: { ru: {
@ -549,10 +559,15 @@ const resources = {
"not-found": "Разговор не найден", "not-found": "Разговор не найден",
"not-found-description": "not-found-description":
"Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален", "Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален",
manage: "Управление обменом",
"sync-error": "Ошибка синхронизации",
name: "Название разговора",
time: "Время",
action: "Действие",
}, },
docs: { docs: {
title: "Открыть документы", title: "Открыть документы",
} },
}, },
}, },
}; };

View File

@ -173,7 +173,7 @@ function Generation() {
console.debug( console.debug(
`[generation] create generation request (prompt: ${prompt}, model: ${model})`, `[generation] create generation request (prompt: ${prompt}, model: ${model})`,
); );
return manager.generateWithBlock(prompt, model,); return manager.generateWithBlock(prompt, model);
}} }}
/> />
</div> </div>

View File

@ -0,0 +1,137 @@
import "../assets/share-manager.less";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import {dialogSelector, dataSelector, syncData, deleteData} from "../store/sharing.ts";
import { useToast } from "../components/ui/use-toast.ts";
import { selectInit } from "../store/auth.ts";
import {useEffectAsync} from "../utils.ts";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from "../components/ui/dialog.tsx";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../components/ui/table.tsx";
import {closeDialog, setDialog} from "../store/sharing.ts";
import {Button} from "../components/ui/button.tsx";
import {useMemo} from "react";
import {Eye, MoreHorizontal, Trash2} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "../components/ui/dropdown-menu.tsx";
import {getSharedLink, SharingPreviewForm} from "../conversation/sharing.ts";
type ShareTableProps = {
data: SharingPreviewForm[];
}
function ShareTable({ data }: ShareTableProps) {
const { t } = useTranslation();
const dispatch = useDispatch();
const time = useMemo(() => {
return data.map((row) => {
const date = new Date(row.time);
return `${date.getMonth()}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
});
}, [data]);
return (
<Table className={`mt-5`}>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>{t('share.name')}</TableHead>
<TableHead>{t('share.time')}</TableHead>
<TableHead>{t('share.action')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, idx) => (
<TableRow key={idx}>
<TableCell>{row.conversation_id}</TableCell>
<TableCell>{row.name}</TableCell>
<TableCell className={`whitespace-nowrap`}>{time[idx]}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={`outline`} size={`icon`}>
<MoreHorizontal className={`h-4 w-4`} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align={`center`}>
<DropdownMenuItem onClick={() => {
window.open(getSharedLink(row.hash), '_blank');
}}>
<Eye className={`h-4 w-4 mr-1`} />
{t("share.view")}
</DropdownMenuItem>
<DropdownMenuItem onClick={async () => {
await deleteData(dispatch, row.hash);
}}>
<Trash2 className={`h-4 w-4 mr-1`} />
{t("conversation.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
function ShareManagement() {
const { t } = useTranslation();
const dispatch = useDispatch();
const open = useSelector(dialogSelector);
const data = useSelector(dataSelector);
const { toast } = useToast();
const init = useSelector(selectInit);
useEffectAsync(async () => {
if (init) {
const resp = await syncData(dispatch);
if (resp) {
toast({
title: t("share.sync-error"),
description: resp,
});
}
}
}, [init]);
return (
<Dialog open={open} onOpenChange={(open) => dispatch(setDialog(open))}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("share.manage")}</DialogTitle>
<DialogDescription className={`share-table`}>
<ShareTable data={data} />
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant={`outline`} onClick={() => dispatch(closeDialog())}>
{t("close")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export default ShareManagement;

View File

@ -116,7 +116,9 @@ function Sharing() {
<HelpCircle className={`w-12 h-12 mb-2.5`} /> <HelpCircle className={`w-12 h-12 mb-2.5`} />
<p className={`title`}>{t("share.not-found")}</p> <p className={`title`}>{t("share.not-found")}</p>
<p className={`description`}>{t("share.not-found-description")}</p> <p className={`description`}>{t("share.not-found-description")}</p>
<Button className={`mt-4`} onClick={() => router.navigate("/")}>{t("home")}</Button> <Button className={`mt-4`} onClick={() => router.navigate("/")}>
{t("home")}
</Button>
</div> </div>
)} )}
</div> </div>

View File

@ -1,5 +1,5 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import {ConversationInstance, Model} from "../conversation/types.ts"; import { ConversationInstance, Model } from "../conversation/types.ts";
import { Message } from "../conversation/types.ts"; import { Message } from "../conversation/types.ts";
import { insertStart } from "../utils.ts"; import { insertStart } from "../utils.ts";
import { RootState } from "./index.ts"; import { RootState } from "./index.ts";
@ -14,8 +14,10 @@ type initialStateType = {
}; };
function GetModel(model: string | undefined | null): string { function GetModel(model: string | undefined | null): string {
return model && supportModels.filter((item: Model) => item.id === model).length return model &&
? model : supportModels[0].id; supportModels.filter((item: Model) => item.id === model).length
? model
: supportModels[0].id;
} }
const chatSlice = createSlice({ const chatSlice = createSlice({

View File

@ -6,6 +6,7 @@ import quotaReducer from "./quota";
import packageReducer from "./package"; import packageReducer from "./package";
import subscriptionReducer from "./subscription"; import subscriptionReducer from "./subscription";
import apiReducer from "./api"; import apiReducer from "./api";
import sharingReducer from "./sharing";
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
@ -16,6 +17,7 @@ const store = configureStore({
package: packageReducer, package: packageReducer,
subscription: subscriptionReducer, subscription: subscriptionReducer,
api: apiReducer, api: apiReducer,
sharing: sharingReducer,
}, },
}); });

69
app/src/store/sharing.ts Normal file
View File

@ -0,0 +1,69 @@
import { createSlice } from "@reduxjs/toolkit";
import { AppDispatch, RootState } from "./index.ts";
import {
deleteSharing,
listSharing,
SharingPreviewForm,
} from "../conversation/sharing.ts";
export const sharingSlice = createSlice({
name: "sharing",
initialState: {
dialog: false,
data: [] as SharingPreviewForm[],
},
reducers: {
toggleDialog: (state) => {
state.dialog = !state.dialog;
},
setDialog: (state, action) => {
state.dialog = action.payload as boolean;
},
openDialog: (state) => {
state.dialog = true;
},
closeDialog: (state) => {
state.dialog = false;
},
setData: (state, action) => {
state.data = action.payload as SharingPreviewForm[];
},
removeData: (state, action) => {
const hash = action.payload as string;
state.data = state.data.filter((item) => item.hash !== hash);
},
},
});
export const {
toggleDialog,
setDialog,
openDialog,
closeDialog,
setData,
removeData,
} = sharingSlice.actions;
export default sharingSlice.reducer;
export const dialogSelector = (state: RootState): boolean =>
state.sharing.dialog;
export const dataSelector = (state: RootState): SharingPreviewForm[] =>
state.sharing.data;
export const syncData = async (dispatch: AppDispatch): Promise<string> => {
const response = await listSharing();
if (response.status) dispatch(setData(response.data));
return response.status ? "" : response.message;
};
export const deleteData = async (
dispatch: AppDispatch,
hash: string,
): Promise<string> => {
const response = await deleteSharing(hash);
if (response.status) dispatch(removeData(hash));
return response.status ? "" : response.message;
};

View File

@ -162,3 +162,56 @@ func ViewAPI(c *gin.Context) {
"data": shared, "data": shared,
}) })
} }
func ListSharingAPI(c *gin.Context) {
user := auth.GetUser(c)
if user == nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": "user not found",
})
return
}
db := utils.GetDBFromContext(c)
data := ListSharedConversation(db, user)
c.JSON(http.StatusOK, gin.H{
"status": true,
"message": "",
"data": data,
})
}
func DeleteSharingAPI(c *gin.Context) {
user := auth.GetUser(c)
if user == nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": "user not found",
})
return
}
db := utils.GetDBFromContext(c)
hash := strings.TrimSpace(c.Query("hash"))
if hash == "" {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": "invalid hash",
})
return
}
if err := DeleteSharedConversation(db, user, hash); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
"message": "",
})
}

View File

@ -8,7 +8,11 @@ func Register(app *gin.Engine) {
router.GET("/list", ListAPI) router.GET("/list", ListAPI)
router.GET("/load", LoadAPI) router.GET("/load", LoadAPI)
router.GET("/delete", DeleteAPI) router.GET("/delete", DeleteAPI)
// share
router.POST("/share", ShareAPI) router.POST("/share", ShareAPI)
router.GET("/view", ViewAPI) router.GET("/view", ViewAPI)
router.GET("/share/list", ListSharingAPI)
router.GET("/share/delete", DeleteSharingAPI)
} }
} }

View File

@ -10,6 +10,13 @@ import (
"time" "time"
) )
type SharedPreviewForm struct {
Name string `json:"name"`
ConversationId int64 `json:"conversation_id"`
Time time.Time `json:"time"`
Hash string `json:"hash"`
}
type SharedForm struct { type SharedForm struct {
Username string `json:"username"` Username string `json:"username"`
Name string `json:"name"` Name string `json:"name"`
@ -73,6 +80,54 @@ func GetSharedMessages(db *sql.DB, userId int64, conversationId int64, refs []st
return messages return messages
} }
func ListSharedConversation(db *sql.DB, user *auth.User) []SharedPreviewForm {
if user == nil {
return nil
}
id := user.GetID(db)
rows, err := db.Query(`
SELECT conversation.conversation_name, conversation.conversation_id, sharing.updated_at, sharing.hash
FROM sharing
INNER JOIN conversation
ON conversation.conversation_id = sharing.conversation_id
AND conversation.user_id = sharing.user_id
WHERE sharing.user_id = ?
ORDER BY sharing.updated_at DESC
LIMIT 100
`, id)
if err != nil {
return nil
}
result := make([]SharedPreviewForm, 0)
for rows.Next() {
var updated []uint8
var form SharedPreviewForm
if err := rows.Scan(&form.Name, &form.ConversationId, &updated, &form.Hash); err != nil {
continue
}
form.Time = *utils.ConvertTime(updated)
result = append(result, form)
}
return result
}
func DeleteSharedConversation(db *sql.DB, user *auth.User, hash string) error {
if user == nil {
return nil
}
id := user.GetID(db)
if _, err := db.Exec(`
DELETE FROM sharing WHERE user_id = ? AND hash = ?
`, id, hash); err != nil {
return err
}
return nil
}
func GetSharedConversation(db *sql.DB, hash string) (*SharedForm, error) { func GetSharedConversation(db *sql.DB, hash string) (*SharedForm, error) {
var shared SharedForm var shared SharedForm
var ( var (