mirror of
https://github.com/usememos/memos.git
synced 2025-02-19 12:50:41 +01:00
chore: implement reaction frontend
This commit is contained in:
parent
e5f244cb50
commit
d86f0bac8c
43
web/src/components/MemoReactionistView.tsx
Normal file
43
web/src/components/MemoReactionistView.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { uniq } from "lodash-es";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { extractUsernameFromName, useUserStore } from "@/store/v1";
|
||||
import { Memo } from "@/types/proto/api/v2/memo_service";
|
||||
import { Reaction, Reaction_Type } from "@/types/proto/api/v2/reaction_service";
|
||||
import { User } from "@/types/proto/api/v2/user_service";
|
||||
import ReactionSelector from "./ReactionSelector";
|
||||
import ReactionView from "./ReactionView";
|
||||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
reactions: Reaction[];
|
||||
}
|
||||
|
||||
const MemoReactionListView = (props: Props) => {
|
||||
const { memo, reactions } = props;
|
||||
const userStore = useUserStore();
|
||||
const [reactionGroup, setReactionGroup] = useState<Map<Reaction_Type, User[]>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const reactionGroup = new Map<Reaction_Type, User[]>();
|
||||
for (const reaction of reactions) {
|
||||
const user = await userStore.getOrFetchUserByUsername(extractUsernameFromName(reaction.creator));
|
||||
const users = reactionGroup.get(reaction.reactionType) || [];
|
||||
users.push(user);
|
||||
reactionGroup.set(reaction.reactionType, uniq(users));
|
||||
}
|
||||
setReactionGroup(reactionGroup);
|
||||
})();
|
||||
}, [reactions]);
|
||||
|
||||
return (
|
||||
<div className="w-full mt-2 flex flex-row justify-start items-start flex-wrap gap-1">
|
||||
<ReactionSelector memo={memo} />
|
||||
{Array.from(reactionGroup).map(([reactionType, users]) => {
|
||||
return <ReactionView key={`${reactionType.toString()} ${users.length}`} memo={memo} reactionType={reactionType} users={users} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MemoReactionListView);
|
@ -8,11 +8,11 @@ import Icon from "./Icon";
|
||||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
relationList: MemoRelation[];
|
||||
relations: MemoRelation[];
|
||||
}
|
||||
|
||||
const MemoRelationListView = (props: Props) => {
|
||||
const { memo, relationList } = props;
|
||||
const { memo, relations: relationList } = props;
|
||||
const memoStore = useMemoStore();
|
||||
const [referencingMemoList, setReferencingMemoList] = useState<Memo[]>([]);
|
||||
const [referencedMemoList, setReferencedMemoList] = useState<Memo[]>([]);
|
||||
|
@ -20,6 +20,7 @@ import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||
import Icon from "./Icon";
|
||||
import MemoContent from "./MemoContent";
|
||||
import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
|
||||
import MemoReactionistView from "./MemoReactionistView";
|
||||
import MemoRelationListView from "./MemoRelationListView";
|
||||
import MemoResourceListView from "./MemoResourceListView";
|
||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||
@ -265,7 +266,8 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
||||
onClick={handleMemoContentClick}
|
||||
/>
|
||||
<MemoResourceListView resources={memo.resources} />
|
||||
<MemoRelationListView memo={memo} relationList={referenceRelations} />
|
||||
<MemoRelationListView memo={memo} relations={referenceRelations} />
|
||||
<MemoReactionistView memo={memo} reactions={memo.reactions} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
79
web/src/components/ReactionSelector.tsx
Normal file
79
web/src/components/ReactionSelector.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { Dropdown, Menu, MenuButton } from "@mui/joy";
|
||||
import { useRef, useState } from "react";
|
||||
import useClickAway from "react-use/lib/useClickAway";
|
||||
import Icon from "@/components/Icon";
|
||||
import { memoServiceClient } from "@/grpcweb";
|
||||
import { MemoNamePrefix, useMemoStore } from "@/store/v1";
|
||||
import { Memo } from "@/types/proto/api/v2/memo_service";
|
||||
import { Reaction_Type } from "@/types/proto/api/v2/reaction_service";
|
||||
import { stringifyReactionType } from "./ReactionView";
|
||||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
}
|
||||
|
||||
const REACTION_TYPES = [
|
||||
Reaction_Type.THUMBS_UP,
|
||||
Reaction_Type.HEART,
|
||||
Reaction_Type.ROCKET,
|
||||
Reaction_Type.LAUGH,
|
||||
Reaction_Type.EYES,
|
||||
Reaction_Type.THUMBS_DOWN,
|
||||
];
|
||||
|
||||
const ReactionSelector = (props: Props) => {
|
||||
const { memo } = props;
|
||||
const memoStore = useMemoStore();
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useClickAway(containerRef, () => {
|
||||
setOpen(false);
|
||||
});
|
||||
|
||||
const handleReactionClick = async (reaction: Reaction_Type) => {
|
||||
try {
|
||||
await memoServiceClient.upsertMemoReaction({
|
||||
id: memo.id,
|
||||
reaction: {
|
||||
contentId: `${MemoNamePrefix}${memo.id}`,
|
||||
reactionType: reaction,
|
||||
},
|
||||
});
|
||||
await memoStore.getOrFetchMemoById(memo.id, {
|
||||
skipCache: true,
|
||||
});
|
||||
} catch (error) {
|
||||
// skip error.
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown open={open} onOpenChange={(_, isOpen) => setOpen(isOpen)}>
|
||||
<MenuButton slots={{ root: "div" }} slotProps={{}}>
|
||||
<span className="h-7 w-7 flex justify-center items-center rounded-full border dark:border-zinc-700 hover:opacity-80">
|
||||
<Icon.Smile className="w-4 h-4 mx-auto dark:text-gray-400" />
|
||||
</span>
|
||||
</MenuButton>
|
||||
<Menu className="relative text-sm" component="div" size="sm" placement="bottom-start">
|
||||
<div ref={containerRef}>
|
||||
<div className="flex-row justify-start items-start py-0.5 px-2 h-auto font-mono space-x-1">
|
||||
{REACTION_TYPES.map((reactionType) => {
|
||||
return (
|
||||
<div
|
||||
key={reactionType}
|
||||
className="inline-flex w-auto cursor-pointer rounded text-lg px-1 text-gray-500 dark:text-gray-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||
onClick={() => handleReactionClick(reactionType)}
|
||||
>
|
||||
{stringifyReactionType(reactionType)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactionSelector;
|
100
web/src/components/ReactionView.tsx
Normal file
100
web/src/components/ReactionView.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import { Tooltip } from "@mui/joy";
|
||||
import classNames from "classnames";
|
||||
import { memoServiceClient } from "@/grpcweb";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { MemoNamePrefix, useMemoStore } from "@/store/v1";
|
||||
import { Memo } from "@/types/proto/api/v2/memo_service";
|
||||
import { Reaction_Type } from "@/types/proto/api/v2/reaction_service";
|
||||
import { User } from "@/types/proto/api/v2/user_service";
|
||||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
reactionType: Reaction_Type;
|
||||
users: User[];
|
||||
}
|
||||
|
||||
export const stringifyReactionType = (reactionType: Reaction_Type): string => {
|
||||
switch (reactionType) {
|
||||
case Reaction_Type.EYES:
|
||||
return "👀";
|
||||
case Reaction_Type.HEART:
|
||||
return "💗";
|
||||
case Reaction_Type.LAUGH:
|
||||
return "😂";
|
||||
case Reaction_Type.ROCKET:
|
||||
return "🚀";
|
||||
case Reaction_Type.THUMBS_DOWN:
|
||||
return "👎";
|
||||
case Reaction_Type.THUMBS_UP:
|
||||
return "👍";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const stringifyUsers = (users: User[]): string => {
|
||||
if (users.length === 0) {
|
||||
return "";
|
||||
}
|
||||
if (users.length < 5) {
|
||||
return users.map((user) => user.nickname || user.username).join(", ");
|
||||
}
|
||||
return `${users
|
||||
.slice(0, 5)
|
||||
.map((user) => user.nickname || user.username)
|
||||
.join(", ")} and ${users.length - 5} others`;
|
||||
};
|
||||
|
||||
const ReactionView = (props: Props) => {
|
||||
const { memo, reactionType, users } = props;
|
||||
const currenUser = useCurrentUser();
|
||||
const memoStore = useMemoStore();
|
||||
const hasReaction = users.some((user) => currenUser && user.username === currenUser.username);
|
||||
|
||||
const handleReactionClick = async () => {
|
||||
if (!currenUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = users.findIndex((user) => user.username === currenUser.username);
|
||||
try {
|
||||
if (index === -1) {
|
||||
await memoServiceClient.upsertMemoReaction({
|
||||
id: memo.id,
|
||||
reaction: {
|
||||
contentId: `${MemoNamePrefix}${memo.id}`,
|
||||
reactionType,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const reactions = memo.reactions.filter(
|
||||
(reaction) => reaction.reactionType === reactionType && reaction.creator === currenUser.name,
|
||||
);
|
||||
for (const reaction of reactions) {
|
||||
await memoServiceClient.deleteMemoReaction({ id: reaction.id });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip error.
|
||||
}
|
||||
await memoStore.getOrFetchMemoById(memo.id, { skipCache: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title={stringifyUsers(users)} placement="top">
|
||||
<div
|
||||
className={classNames(
|
||||
"h-7 border px-2 py-0.5 rounded-full font-memo flex flex-row justify-center items-center gap-1 dark:border-zinc-700",
|
||||
currenUser && "cursor-pointer",
|
||||
hasReaction && "bg-blue-50 border-blue-100 dark:bg-zinc-900",
|
||||
)}
|
||||
onClick={handleReactionClick}
|
||||
>
|
||||
<span>{stringifyReactionType(reactionType)}</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">{users.length}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactionView;
|
@ -139,7 +139,7 @@ const MemoDetail = () => {
|
||||
)}
|
||||
<MemoContent key={`${memo.id}-${memo.updateTime}`} memoId={memo.id} content={memo.content} readonly={readonly} />
|
||||
<MemoResourceListView resources={memo.resources} />
|
||||
<MemoRelationListView memo={memo} relationList={referenceRelations} />
|
||||
<MemoRelationListView memo={memo} relations={referenceRelations} />
|
||||
<div className="w-full mt-3 flex flex-row justify-between items-center gap-2">
|
||||
<div className="flex flex-row justify-start items-center">
|
||||
{!readonly && (
|
||||
|
@ -1,4 +1,5 @@
|
||||
export const UserNamePrefix = "users/";
|
||||
export const MemoNamePrefix = "memos/";
|
||||
|
||||
export const extractUsernameFromName = (name: string = "") => {
|
||||
return name.slice(UserNamePrefix.length);
|
||||
|
Loading…
x
Reference in New Issue
Block a user