Feat like posts (#818)

* add like post api

* add like action

* refactor

* add locale
This commit is contained in:
an-lee
2024-07-17 06:35:14 +08:00
committed by GitHub
parent 3a1467a9c0
commit 8ec2e5dfc4
7 changed files with 198 additions and 98 deletions

View File

@@ -219,6 +219,14 @@ export class Client {
return this.api.delete(`/api/posts/${id}`);
}
likePost(id: string): Promise<PostType> {
return this.api.post(`/api/posts/${id}/like`);
}
unlikePost(id: string): Promise<PostType> {
return this.api.delete(`/api/posts/${id}/unlike`);
}
transcriptions(params?: {
page?: number;
items?: number;

View File

@@ -640,5 +640,6 @@
"examples": "Examples",
"continueLearning": "Continue learning",
"enrollNow": "Enroll now",
"enrollments": "Enrollments"
"enrollments": "Enrollments",
"noLikesYet": "No likes yet"
}

View File

@@ -640,5 +640,6 @@
"examples": "示例",
"continueLearning": "继续练习",
"enrollNow": "加入练习",
"enrollments": "参加的课程"
"enrollments": "参加的课程",
"noLikesYet": "还没有点赞"
}

View File

@@ -13,6 +13,12 @@ import {
AlertDialogFooter,
Button,
toast,
HoverCard,
HoverCardTrigger,
HoverCardContent,
Avatar,
AvatarImage,
AvatarFallback,
} from "@renderer/components/ui";
import { t } from "i18next";
import Markdown from "react-markdown";
@@ -22,15 +28,19 @@ import {
CopyPlusIcon,
PlusCircleIcon,
ChevronRightIcon,
ThumbsUpIcon,
} from "lucide-react";
import { useCopyToClipboard } from "@uidotdev/usehooks";
import { Link } from "react-router-dom";
export const PostActions = (props: { post: PostType }) => {
const { post } = props;
export const PostActions = (props: {
post: PostType;
handleUpdate: (post: PostType) => void;
}) => {
const { post, handleUpdate } = props;
const [_, copyToClipboard] = useCopyToClipboard();
const [copied, setCopied] = useState<boolean>(false);
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext);
const [asking, setAsking] = useState<boolean>(false);
const [aiReplies, setAiReplies] = useState<Partial<MessageType>[]>([]);
@@ -80,96 +90,106 @@ export const PostActions = (props: { post: PostType }) => {
}
};
const toggleLike = async () => {
if (post.liked) {
webApi
.unlikePost(post.id)
.then((p) => handleUpdate(p))
.catch((err) => toast.error(err.message));
} else {
webApi
.likePost(post.id)
.then((p) => handleUpdate(p))
.catch((err) => toast.error(err.message));
}
};
return (
<>
<div className="flex items-center space-x-2 justify-end">
{post.target && post.targetType === "Medium" && (
<AlertDialog>
<AlertDialogTrigger asChild>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 justify-end">
<HoverCard>
<HoverCardTrigger asChild>
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("addToLibary")}
data-tooltip-place="bottom"
variant="ghost"
size="sm"
className="px-1.5 rounded-full"
onClick={toggleLike}
>
<PlusCircleIcon className="w-5 h-5 text-muted-foreground hover:text-primary" />
<ThumbsUpIcon
className={`w-5 h-5 ${
post.liked ? "text-red-600" : "text-muted-foreground"
}`}
/>
{typeof post.likesCount === "number" && post.likesCount > 0 && (
<span className="ml-1 text-sm">{post.likesCount}</span>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("addResource")}</AlertDialogTitle>
<AlertDialogDescription>
{(post.target as MediumType).mediumType === "Video" &&
t("areYouSureToAddThisVideoToYourLibrary")}
</HoverCardTrigger>
<HoverCardContent>
<div className="max-h-48 overflow-y-auto">
{post.likeByUsers?.length === 0 && (
<div className="text-center text-muted-foreground">
{t("noLikesYet")}
</div>
)}
<div className="grid grid-cols-6 gap-2">
{post.likeByUsers?.map((user) => (
<Link
key={user.id}
to={`/users/${user.id}`}
className="aspect-square"
>
<Avatar className="w-full h-full">
<AvatarImage src={user.avatarUrl} />
<AvatarFallback className="text-xl">
{user.name[0].toUpperCase()}
</AvatarFallback>
</Avatar>
</Link>
))}
</div>
</div>
</HoverCardContent>
</HoverCard>
</div>
<div className="flex items-center space-x-2 justify-end">
{post.target && post.targetType === "Medium" && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("addToLibary")}
data-tooltip-place="bottom"
variant="ghost"
size="sm"
className="px-1.5 rounded-full"
>
<PlusCircleIcon className="w-5 h-5 text-muted-foreground hover:text-primary" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("addResource")}</AlertDialogTitle>
<AlertDialogDescription>
{(post.target as MediumType).mediumType === "Video" &&
t("areYouSureToAddThisVideoToYourLibrary")}
{(post.target as MediumType).mediumType === "Audio" &&
t("areYouSureToAddThisAudioToYourLibrary")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleAddMedium}>
{t("confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
{(post.target as MediumType).mediumType === "Audio" &&
t("areYouSureToAddThisAudioToYourLibrary")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleAddMedium}>
{t("confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
{typeof post.metadata?.content === "string" && (
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("copy")}
data-tooltip-place="bottom"
variant="ghost"
size="sm"
className="px-1.5 rounded-full"
>
{copied ? (
<CheckIcon className="w-5 h-5 text-green-500" />
) : (
<CopyPlusIcon
className="w-5 h-5 text-muted-foreground hover:text-primary"
onClick={() => {
copyToClipboard(post.metadata.content as string);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 3000);
}}
/>
)}
</Button>
)}
{post.metadata?.type === "prompt" && (
<ConversationShortcuts
open={asking}
onOpenChange={setAsking}
prompt={post.metadata.content as string}
onReply={(replies) => {
setAiReplies([...aiReplies, ...replies]);
setAsking(false);
}}
trigger={
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("sendToAIAssistant")}
data-tooltip-place="bottom"
variant="ghost"
size="sm"
className="px-1.5 rounded-full"
>
<BotIcon className="w-5 h-5 text-muted-foreground hover:text-primary" />
</Button>
}
/>
)}
{post.metadata?.type === "gpt" && (
<>
{typeof post.metadata?.content === "string" && (
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("copy")}
@@ -184,7 +204,7 @@ export const PostActions = (props: { post: PostType }) => {
<CopyPlusIcon
className="w-5 h-5 text-muted-foreground hover:text-primary"
onClick={() => {
copyToClipboard((post.metadata.content as { [key: string]: any }).configuration.roleDefinition as string);
copyToClipboard(post.metadata.content as string);
setCopied(true);
setTimeout(() => {
setCopied(false);
@@ -193,21 +213,76 @@ export const PostActions = (props: { post: PostType }) => {
/>
)}
</Button>
)}
<Link to={`/conversations?postId=${post.id}`}>
{post.metadata?.type === "prompt" && (
<ConversationShortcuts
open={asking}
onOpenChange={setAsking}
prompt={post.metadata.content as string}
onReply={(replies) => {
setAiReplies([...aiReplies, ...replies]);
setAsking(false);
}}
trigger={
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("sendToAIAssistant")}
data-tooltip-place="bottom"
variant="ghost"
size="sm"
className="px-1.5 rounded-full"
>
<BotIcon className="w-5 h-5 text-muted-foreground hover:text-primary" />
</Button>
}
/>
)}
{post.metadata?.type === "gpt" && (
<>
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("saveAiAssistant")}
data-tooltip-content={t("copy")}
data-tooltip-place="bottom"
variant="ghost"
size="sm"
className="px-1.5 rounded-full"
>
<PlusCircleIcon className="w-5 h-5 text-muted-foreground hover:text-primary" />
{copied ? (
<CheckIcon className="w-5 h-5 text-green-500" />
) : (
<CopyPlusIcon
className="w-5 h-5 text-muted-foreground hover:text-primary"
onClick={() => {
copyToClipboard(
(post.metadata.content as { [key: string]: any })
.configuration.roleDefinition as string
);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 3000);
}}
/>
)}
</Button>
</Link>
</>
)}
<Link to={`/conversations?postId=${post.id}`}>
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("saveAiAssistant")}
data-tooltip-place="bottom"
variant="ghost"
size="sm"
className="px-1.5 rounded-full"
>
<PlusCircleIcon className="w-5 h-5 text-muted-foreground hover:text-primary" />
</Button>
</Link>
</>
)}
</div>
</div>
{aiReplies.length > 0 && <AIReplies replies={aiReplies} />}

View File

@@ -17,8 +17,9 @@ import { Link } from "react-router-dom";
export const PostCard = (props: {
post: PostType;
handleDelete: (id: string) => void;
handleUpdate: (post: PostType) => void;
}) => {
const { post, handleDelete } = props;
const { post, handleDelete, handleUpdate } = props;
const { user } = useContext(AppSettingsProviderContext);
return (
@@ -100,14 +101,12 @@ export const PostCard = (props: {
{post.targetType == "Note" && (
<>
<div className="text-xs text-muted-foreground">
{t("sharedNote")}
</div>
<div className="text-xs text-muted-foreground">{t("sharedNote")}</div>
<PostNote note={post.target as NoteType} />
</>
)}
<PostActions post={post} />
<PostActions post={post} handleUpdate={handleUpdate} />
</div>
);
};

View File

@@ -137,7 +137,20 @@ export const Posts = (props: { userId?: string; by?: string }) => {
<div className="space-y-6">
{posts.map((post) => (
<div key={post.id}>
<PostCard post={post} handleDelete={handleDelete} />
<PostCard
post={post}
handleDelete={handleDelete}
handleUpdate={(post) => {
const updatedPosts = posts.map((p) => {
if (p.id === post.id) {
return Object.assign(p, post);
} else {
return p;
}
});
setPosts(updatedPosts);
}}
/>
<Separator />
</div>
))}

View File

@@ -12,6 +12,9 @@ type PostType = {
targetType?: string;
targetId?: string;
target?: MediumType | StoryType | RecordingType | NoteType;
liked?: boolean;
likesCount?: number;
likeByUsers?: UserType[];
createdAt: Date;
updatedAt: Date;
};